Java> Java核心卷读书笔记 - 网络

连接到服务器

telnet手动连接

对于Windows,要想使用telnet工具,需要先启动telnet服务。
启动方法:控制面板->程序->启用或关闭Windows功能->选择并启用Telnet Client(Telnet客户端)
连接示例,命令行输入

telnet time-a.nist.gov 13

得到类似下图:

命令行命令请求与远程服务器time-a.nist.gov 进行通讯,端口号13。

Java连接服务器

socket连接失败,会抛出异常UnknowHostException;如果是其他文件,抛出IOException。
Java连接服务器简单示例

/**
* Socket连接远程地址, 获取流之后, 直接打印每一行. 一直持续到流发送完毕且服务器断开连接
*/
try(Socket socket = new Socket("time-a.nist.gov", 13);  // 新建套接字, 负责启动该进程内部和外部之间的通信. 远程地址: "time-a.nist.gov"; 13: 端口号
Scanner in = new Scanner(socket.getInputStream(), "UTF-8")) {  
      while(in.hasNext()) {
            String line = in.next();
            System.out.println(line);
      }
}

套接字超时

  1. 没有数据导致超时
    从套接字读取信息,读操作可能会阻塞,直到有数据可读。
    可以根据需要,设置合理的超时值,来解决这个问题
Socket s = new Socket();
s.setSoTimeout(1000); // 10秒后超时
  1. 读写操作超时
    如果已经为套接字设置超时值,不过之后的读/写操作没有完成之前就超过时间限制,那么这些操作会抛出SocketTimeOutException异常。可以捕获异常,并对超时做出反应。
try {
      InputStream in = s.getInputStream();
      ...
}
catch (InterruptedIOException e) {
      // react to timeout
}
  1. 连接超时
    构造器Socket(String host, int port)会无限期阻塞下去,直到与主机建立初始连接。
    可以先构建一个无连接的套接字,然后使用超时来进行连接的方式解决该问题
Socket s = new Socket();
s.connect(new InetSocketAddress(host, port), timeout);
  1. Socket 常用API
    java.net.Socket 1.0
函数原型 加入JDK版本 描述
Socket() 1.1 创建一个还没有被连接的套接字
void connect(SocketAddress address) 1.4 将该套接字连接到给定地址
void connect(SocketAddress address, int timeInMilliseconds) 1.4 将套接字连接到给定地址。如果给定时间内没有响应,则返回
void setSoTimeout(int timeoutInMilliseconds) 1.1 设置套接字上读请求的阻塞时间。如果超出给定时间,则抛出InterruptedIOException异常
boolean isConnected() 1.4 如果套接字已经被连接,返回true
boolean isClosed() 1.4 如果套接字已被关闭,返回true

因特网地址(IP地址)

IP地址是一连串数字表示的主机地址,分为IPv4和IPv6两类。IPv4:IP地址4byte,如192.168.0.1;IPv6:IP地址16byte。
InetAddress类:需要在主机和因特网之间转换时使用

InetAddress功能

  1. 获取主机IP地址
InetAddress address = InetAddress.getByName("time-a.nist.gov"); // getByName返回代表某个主机的InetAddress对象, 该对象封装了IP地址
byte[] b = address.getAddress(); // 获取IP地址(4byte数据形式)
  1. 获取多个主机IP地址
    同一个域名主机,可能对应多个IP地址,因为主机为了实现负载均衡,包含多个IP地址。访问主机时,随机返回其中一个。
    获取所有主机IP地址
InetAddress[] addresses = InetAddress.getAllByName(host); // host是主机名
  1. 获取本机地址
    本地回环地址,对因特网上其他主机无用
InetAddress address = InetAddressgetByName("localhost"); // localhost代表本地回环地址, 127.0.0.1

InetAddress.getLoopbackAddress(); // 第二种获取本地回环地址方式

本地主机地址

InetAddress address = InetAddress.getLocalHost();

InetAddress示例

    public static void main(String[] args) throws UnknownHostException {
        String host = "www.horstmann.com";
        InetAddress[] addresses = InetAddress.getAllByName(host);
        for (InetAddress a : addresses) {
            System.out.println(a);
        }

        System.out.println("local address = " + InetAddress.getLocalHost());
        System.out.println("loop back address = " + InetAddress.getLoopbackAddress());
    }

示例运行结果

www.horstmann.com/204.44.192.29
local address = DESKTOP-H0C09BT/172.16.6.131
loop back address = localhost/127.0.0.1

服务器

服务器套接字

服务器启动后,会等待某个客户端连接到它的端口。ServerSocket类用于创建服务器套接字。

服务器套接字ServerSocket跟前面提到的Socket套接字,是什么关系?
ServerSocket是用于服务器的,Socket是用于客户端的。服务器是等待客户端连接,客户端是通过网络请求连接远程服务器。

  1. 创建服务器套接字
ServerSocket s = new ServerSockt(8189); // 建立负责监控8189端口的服务器, 选择8189是因为没有别的程序/服务在使用该端口
  1. 等待客户端连接
Socket incoming = s.accept(); // 告诉程序不停等待, 直到有客户端连接监听的8189端口
  1. 服务器通过输入输出流与客户端通信
    服务器发送给服务器输出流的所有信息,都会成为客户端的输入;
    来自客户端的输出流,都会成为服务器端的输入;
// 获取服务器输入流
InputStream inStream = incoming.getInputStream();
// 获取服务器输出流
OutputStream outStream = incoming.getInputStream();

// 将流包装成扫描器和写入器, 通过套接字发送文本
Scanner in = new Scanner(inStream, "UTF-8");
PrintWriter out = new PrintWriter(new OutputStreamWriter(outStream, "UTF-8"), true /* auto flush */);

// 向客户端发送一条信息
out.println("hello! Enter BYE to exit.");

// 关闭连接进来的套接字
incoming.close();

服务器程序框架

每个服务器,都会不间断执行下面的循环:

  1. 通过输入数据流从客户端接收一个命令;
  2. 解码客户端命令;
  3. 收集客户端请求信息;
  4. 通过输出数据流发送信息给客户端;

服务器示例

例子演示服务器程序框架,如何创建ServerSocket,等待连接,以及如何向客户端发送消息。

try(ServerSocket s = new ServerSocket(8189)) {
      try (Socket incoming = s.accept()) { // 阻塞, 直到有客户端连接到服务器
            InputStream inStream = incoming.getInputStream();
            OutputStream outStream = incoming.getOutputStream();
            
            // 用Scanner包装输入流, PrintWriter包装输出流
            try (Scanner in = new Scanner(inStream, "UTF-8")) {
                  PrintWriter out = new PrintWriter(new OutputStreamWriter(outStream, "UTF-8"));
                  out.println("Hello! Enter BYE to exit.");
            }

            // 响应客户端输入
            boolean done = false;
            while(!done && in.hasNextLine()) {
                  String line = in.nextLine();
                  out.println("Ehco: " + line);
                  if (line.equals("BYE")) done = true;
            }
      }
}

运行方式:本地命令行模式下输入telnet localhost 8189;远程运行需要本机IP地址或者域名,提到本地回环地址。

为多个客户端服务

前面的简单服务器的例子只能支持一个服务器,如果要支持多个客户端,要怎么办?
可以使用多线程,每当程序建立一个新套接字时(s.accept()成功返回一个Socket),启动一个新的线程来处理服务器和该客户端之间的连接,主程序立即返回并等待下一个连接。
示例

ServerSocket s = new ServeSocket(8189);

// 服务器为每个新建Socket新建一个线程, 处理与具体客户端之间连接问题. 主程序仍然继续等待下一个连接请求
while(true) {
      Socket incoming = s.accept();
      Runnable r = new ThreadedEchoHanlder(incoming);
      
      // 新建线程
      Thread t = new Thread(r);
      // 启动线程
      t.start();
}

public class ThreadedEchoHandler implements Runnable {
      private Socket incoming;
      public ThreadedEchoHandler(Socket incoming) {
            this.incoming = incoming;
      }

      @override
      public void run() {
            try (InputStream inStream = incoming.getInputStream();
            OutputStream outStream = incoming.getOutputStream();) {
                  // 处理输入输出响应
                  // e.g. Scanner包装InputStream, PrintWriter包装OutputStream
                  Scanner in = new Scanner(inStream, "UTF-8");
                  PrintWriter out = new PrintWriter(new OutputStreamWriter(outStream), "UTF-8");
                  
                  // 向客户端发送消息
                  out.println("Hello! Enter BYE to exit.");
                  // 响应客户端输入, 以收到客户端"BYE"字符串为结束
                  boolean done = false;
                  while (!done && in.hasNextLine()) {
                        String line = in.nextLine();
                        out.println("Echo: " + line);
                        if (line.trim().equals("BYE")) done = true;
                  }
            } catch (IOException e) {
                  e.printTraceStack();
            }
      }
}

半关闭

半关闭(half close),简单说,就是只收不发,或者只发不收。
典型应用场景:向服务器传输数据,一开始不知道要传多少; 传文件,向文件写数据写完后即可关闭。
如果不这样做,要么关闭套接字,这样与服务器连接断开;要么保持套接字打开,不过这样仍然占用着输出流资源。

客户端使用半关闭方法示例:

String host = "127.0.0.1";
long port = 8189;

try (Socket socket = new Socket(host, port)) {
      Scanner in = new Scanner(socket.getInputStream(), "UTF-8");
      PrintWriter writer = new PrintWriter(socket.getOutputStream());
      // 向服务器发送请求数据
      writer.print("last data");
      writer.flush();

      socket.shutdownOutput(); // 关闭输出, 这之后客户端(套接字)进入半关闭状态
      // 读取服务器数据
      while (in.hasNextLine()) {
            String line = in.nextLine();
            // 处理line
      }
}

可中断套接字

两种常件阻塞情况

  • 当连接到一个套接字时,当前线程会阻塞,直到连接建立或超时;
  • 通过套接字读写数据时,当前线程阻塞,直到操作成功或者超时;

如果先网络读取速度过慢,发生阻塞的时候,如果用户想取消怎么办?
这就需要中断套接字 -- 套接字通道SocketChannel类。

使用SocketChannel

  1. 打开SocketChannel
String host = "localhost"; // 示例, 本地主机
long port = 8189; // 示例, 选择一个未被占用的端口号
SocketChannel channel = SocketChannel.open(new InetSocketAddress(host, port));
  1. 从SocketChannel读取信息
    用Scanner类从SocketChannel读取信息,可以避免处理缓冲区,不过这不是强制性的。
Scanner in = newScanner(channel, "UTF-8");

3.将通道转换成输出流

OutputStream outStream =Channels.newOutputStream(channel);

当线程正在执行打开、读取或写入操作时,如果发生中断,那么这些操作将不会阻塞,而是抛出异常结束。

*** 注解:看到这里,还是不明白,中断套接字到底有什么用?怎么用? ***

获取Web数

URL , URI

URL: Uniform Resource Locator, 统一资源定位符,包含了用于定位Web资源的足够信息,是URI的特例;
URI: Uniform Resource Identifier,统一资源标识符,是纯粹的语法结构,用来指定Web资源的字符串的各种组成部分;
URN: mailto:cay@horstmann.com属于URI,但不属于URL,因为根据该标识符无法定位任何数据。这样的URI称为URN(Uniform Resource Name),统一资源名称。

Java URI类不包含任何处理资源的方法,唯一作用是解析。Java URL类可以打开一个到资源的流,只能作用于:http, https, ftp, 本地文件系统(file:)和JAR文件(jar:)。

  1. URI类解析标识符
    解析标识符,并分解成各种不同的部分
getScheme
getSchemeSpecificPart
getAuthority
getUserInfo
getHost
getPort
getPath
getQuery
getFragment
  1. URI类处理绝对标识符和相对标识符
  • 解析相对URI
    比如将绝对URI http://docs.mycompany.com/api/java/net/ServerSocket.html和相对URI../../java/net/Socket.html#Socket(),组合成绝对URIhttp://docs.mycompany.com/api/java/net/Socket.htm#Socket()
  • 相对化relativization
    比如一个URIhttp://docs.mycompany.com/api和另外一个URIhttp://docs.mycompany.com/api/java/lang/String.html相对化之后,得到新URIjava/lang/String.html

使用URLConnection获取信息

利用URLConnection类,能比URL类获取更多Web资源信息和控制功能。

  • URLConnection使用步骤:
  1. 调用URL类的openConnection() 获得URLConnection对象
URLConnection connection = url.openConnection();
  1. 设置请求属性
setDoInput
setDoOutput
setIfModifiedSince
setUseCaches
setAllowUserInteraction
setRequestProperty
setConnectTimeout
setReadTimeout
  1. 调用connect连接远程资源
connection.connect(); // 除与服务器建立连接套接字外, 该方法还可以用于向服务器查询头信息(header information)
  1. 与服务器建立连接后,可以查询头信息
    getHeaderFieldKey和getHeaderField枚举了消息头的所有字段,getHeaderFields包含了消息头中所有字段的标准Map对象。
getContentType
getContentLength
getContentEncoding
getDate
getExpiration
getLastModified

getHeaderFieldKey和getHeaderField,以及getHeaderFields查询消息头信息的用法示例

// 必要步骤: 创建URL并连接服务器
String urlName = "http://www.163.com";
URL url = new URL(urlName);
URLConnection connection = url.openConnection();
connection.connect();

// 打印消息头信息
/* 方式一: 使用getHeaderFieldKey和getHeaderField 获取消息头信息, 多个需要循环获取, 直到key = null */
int n = 0;
String key = connection.getHeaderFieldKey(n);
String value = connection.getHeaderField(n);

/*方式二: 使用getHeaderFields来获取消息头 */
Map<String, List<String>> headers= connection.getHeaderFields();
headers.forEach((k ,v)->{
      for (String s : v) {
            System.out.println(k + ": " + s);
      }
});

// 打印消息头具体字段信息
System.out.println("---------");
System.out.println("getContentType: " + connection.getContentType());
System.out.println("getContentLength: " + connection.getContentLength());
System.out.println("getContentEncoding: " + connection.getContentEncoding());
System.out.println("getDate: " + connection.getDate());
System.out.println("getExpiration: " + connection.getExpiration());
System.out.println("getLastModified: " + connection.getLastModified());
System.out.println("---------");

String encoding = connection.getContentEncoding();
if (encoding == null) encoding = "UTF-8";
try (Scanner in = new Scanner(connection.getInputStream(), encoding)) {
      // 打印内容前10行
      for (int i = 1; in.hasNextLine() && i <= 10; i++) {
            System.out.println(in.nextLine());
      }
      if(in.hasNextLine()) System.out.println("...");
}
catch(IOException e) {
      e.printStackTrace();
}

5.访问资源数据
getInputStream获取一个输入流以读取信息,该方法与URL类的openstream方法返回流相同。另外一个方法,getContent实际操作中不是很有用。
注意:URLConnection的getInputStream和getOutputStream方法,与Socket中这些方法并不相同,特别是在处理请求和响应消息头时。

connection.getInputStream() 等价于 url.openStream()

connection.getInputStream() 不等于 socket.getInputStream()
  • URLConnection一些重要方法
    默认情况下,建立连接只产生从服务器读取信息的输入流,并不产生任何执行写操作的输出流。如果想获得输出流(如向Web服务器提交数据),需要调用:
connection.setDoOutput(true);

// 其他重要方法
setIfModifiedSince // 告诉连接, 只对某个自特定日期以来被修改过的数据感兴趣

setRequestProperty // 用来设置对特定协议起作用的任何“名-值”对(name/value)

/* 只作用于Applet */
setUserCaches // 用于命令浏览器首先检查它的缓存
setAllowUserInteraction // 用于在访问有密码保护的资源时弹出对话框, 以便查询用户名和口令

访问一个带有密码保护Web页的例子,步骤:
1)将用户名、冒号和密码以字符串形式连接在一起

String input = username + ":" + password;

2)计算上一步字符串Base64编码,用于将字节序列编码成可打印的ASCII字符序列。

Base64.Encoder encoder = Base64.getEncoder();
String encoding = encoder.encodeToString(input.getBytes(StandardCharsets.UTF-8));

3)用"Authorization"这个名字和"Basic" + encoding的值调用setRequestProperty方法

connection.setRequestProperty("Authorization", "Basic" + encoding);

带有密码保护Web页步骤框架:

String url = "http://horstmann.com";
URL url = new URL(urlName);
URLConnection connection = url.openConnection();

String userName = "username"; // 用户名
String password = "password"; // 密码
String input = username + ":" + password;
Base64.Encoder encoder = Base64.getEncoder();
String encoding = encoder.encodeToString(input.getBytes(StandardCharsets.UTF_8));
connection.setRequestProperty(""Authorization", "Basic " + encoding);

conection.connect(); // 连接服务器

//...

错误处理

如果服务器出现错误,调用connection.getInputStream会抛出异常FileNotFoundException。此时,服务器仍然会向浏览器返回一个错误页面,为了捕捉这个错误页,可以调用getErrorStream,调用方法InputStream err = connection.getErrorStream()

重定向redirect

将POST数据发送给服务器时,服务器端程序产生的响应可能是redirect,后面跟着完全不同URL。
HttpURLConnection类通常可以用来处理这种重定向。当要处理的URL是htpp://或者https://开头,那么URLConnection对象可以强制转换为其子类HttpURLConnection对象。

posted @ 2020-12-01 01:09  明明1109  阅读(127)  评论(0编辑  收藏  举报