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);
}
}
套接字超时
- 没有数据导致超时
从套接字读取信息,读操作可能会阻塞,直到有数据可读。
可以根据需要,设置合理的超时值,来解决这个问题
Socket s = new Socket();
s.setSoTimeout(1000); // 10秒后超时
- 读写操作超时
如果已经为套接字设置超时值,不过之后的读/写操作没有完成之前就超过时间限制,那么这些操作会抛出SocketTimeOutException异常。可以捕获异常,并对超时做出反应。
try {
InputStream in = s.getInputStream();
...
}
catch (InterruptedIOException e) {
// react to timeout
}
- 连接超时
构造器Socket(String host, int port)
会无限期阻塞下去,直到与主机建立初始连接。
可以先构建一个无连接的套接字,然后使用超时来进行连接的方式解决该问题
Socket s = new Socket();
s.connect(new InetSocketAddress(host, port), timeout);
- 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功能
- 获取主机IP地址
InetAddress address = InetAddress.getByName("time-a.nist.gov"); // getByName返回代表某个主机的InetAddress对象, 该对象封装了IP地址
byte[] b = address.getAddress(); // 获取IP地址(4byte数据形式)
- 获取多个主机IP地址
同一个域名主机,可能对应多个IP地址,因为主机为了实现负载均衡,包含多个IP地址。访问主机时,随机返回其中一个。
获取所有主机IP地址
InetAddress[] addresses = InetAddress.getAllByName(host); // host是主机名
- 获取本机地址
本地回环地址,对因特网上其他主机无用
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是用于客户端的。服务器是等待客户端连接,客户端是通过网络请求连接远程服务器。
- 创建服务器套接字
ServerSocket s = new ServerSockt(8189); // 建立负责监控8189端口的服务器, 选择8189是因为没有别的程序/服务在使用该端口
- 等待客户端连接
Socket incoming = s.accept(); // 告诉程序不停等待, 直到有客户端连接监听的8189端口
- 服务器通过输入输出流与客户端通信
服务器发送给服务器输出流的所有信息,都会成为客户端的输入;
来自客户端的输出流,都会成为服务器端的输入;
// 获取服务器输入流
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();
服务器程序框架
每个服务器,都会不间断执行下面的循环:
- 通过输入数据流从客户端接收一个命令;
- 解码客户端命令;
- 收集客户端请求信息;
- 通过输出数据流发送信息给客户端;
服务器示例
例子演示服务器程序框架,如何创建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
- 打开SocketChannel
String host = "localhost"; // 示例, 本地主机
long port = 8189; // 示例, 选择一个未被占用的端口号
SocketChannel channel = SocketChannel.open(new InetSocketAddress(host, port));
- 从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:)。
- URI类解析标识符
解析标识符,并分解成各种不同的部分
getScheme
getSchemeSpecificPart
getAuthority
getUserInfo
getHost
getPort
getPath
getQuery
getFragment
- URI类处理绝对标识符和相对标识符
- 解析相对URI
比如将绝对URIhttp://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使用步骤:
- 调用URL类的openConnection() 获得URLConnection对象
URLConnection connection = url.openConnection();
- 设置请求属性
setDoInput
setDoOutput
setIfModifiedSince
setUseCaches
setAllowUserInteraction
setRequestProperty
setConnectTimeout
setReadTimeout
- 调用connect连接远程资源
connection.connect(); // 除与服务器建立连接套接字外, 该方法还可以用于向服务器查询头信息(header information)
- 与服务器建立连接后,可以查询头信息
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对象。