互联网编程
课程主要内容:
Internet基础知识
网络数据流及JAVA I/O流
线程及多线程服务器编程
Internet地址
URL和URI
HTTP
URL Connection
客户端Socket和服务器端Socket及安全Socket
非阻塞I/O
UDP及IP组播
第一讲:基本网络概念
1.易混淆的几个概念:
Network:最广的,是汉语里面最广义的“网络”,如各种电网、神经网络等。
互联网:因特网和其他类似的由计算机相互连接而成的大型计算机网络系统都是互联网,Internet是全球互联网中最大的一个。
Internet:因特网,是由于许多小的网络(子网)互联而成的一个广域网,每个子网中连接着若干台计算机(主机)。主要由计算机和电缆组成。因为Internet是互联网中最大的一个,所以也常将Internet称为互联网。
Web:万维网。使用超文本技术将遍布全球的各种信息资源链接起来,以便于用户访问。web是Internet上的一个应用层服务, 除了web外,QQ、Email等也是Internet上其他类型的应用层服务。
2.计算机网络:
节点/node:计算机、打印机、路由器、网关等
主机/host:计算机节点
地址/address:IPv4、IPv6、MAC
交换:现代计算机网络都是包交换网络
协议/protocol:通讯规则
3.网络应用程序与网络编程:
网络应用程序(Network Application)是指能够为网络用户提供各种服务的程序,它用于提供或获取网络上的共享资源。如浏览程序、传输程序、远程登录程序等。
网络编程最主要的工作是在发送端把信息通过规定好的协议进行组装包,在接收端按照规定好的协议把包进行解析,从而提取出对应的信息,达到通信的目的。中间最主要的就是数据包的组装,数据包的过滤,数据包的捕获,数据包的分析。最后再做一些处理,包括显示、响应等。代码、开发工具、服务器架设和网页设计等可能都会接触。
4.互联网通信技术基础知识:
网络应用程序之间网络通讯的本质是网络中处于两个主机上的两个进程之间进行通讯。
路由器只有网络层,数据链路层,物理层三层结构。
网络的三种交换技术: 电路交换(线路/电话交换);报文交换;分组交换(包交换)。
(1)电路交换(线路交换):
① 线路建立:发送方提出连接请求,完成逐个结点的接续过程。建立由源站到目的站的传输链路。
② 数据传输:全双工传输
③ 电路拆除:数据传输结束,由源站(目的站)提出终止通信。各结点拆除相应的连接,释放信道资源。
(2)报文交换(存储转发):
① 过程:结点接收一个报文之后,报文缓存(存储设备),根据报文中目的地址转发到下一个结点(如此往复,直到报文到达目的站)。
② 特点:不需要通信双方预先建立专用的数据通路(无需建立链路、拆除链路过程)。
③ 缺点:需要对的完整报文进行存储/转发,结点存储/转发的时延较大,不适用于高实时性通信。
(3)交换技术:分组交换
属于“存储/转发”交换方式;
报文被划分为分组,每个分组独立转发;
数据报交换:任何分组都当作单独的“小报文”处理,以报文交换方式单独处理分组。
虚电路交换:通信双方在开始发送和接收分组之前,需要建立逻辑链路(虚电路)。所有分组都必须沿着事先建立的虚电路传输,需要虚呼叫建立和拆除。
5.网络协议:
网络协议即采用统一的信息交换规则,规定信息格式,规定如何发送和接收信息。
网络协议分层
( 1)将网络的整体功能分解为功能层,用协议规定功能。
( 2)同等功能层之间采用协议进行。
( 3)相邻功能层之间采用接口进行交互。
6.因特网的体系结构:
物理层:透明地传送比特流。
数据链路层:在两个相邻结点间的线路上无差错地传送以帧为单位的数据。
网络层:完成主机间报文传输,选择合适的路由,使发送方报文能够正确无误地按照地址找到目的站,并交付给目的站。
传输层:进行通信的两个进程之间提供一个可靠的端到端的服务。
应用层:确定进程之间通信的性质,满足用户的需要。
网络不同层的协议:
Internet协议标准化组织:
Internet应用层网络编程和协议的标准大多由IETF和W3C制定。
IETF(Internet Engineering Task Force, Internet工程任务组),是不太正式的民间团体,制定的标准包括TCP/IP、MIME、SMTP。
W3C(World Wide Web Consortium,国际互联网协会),是厂商组织,制定的标准包括HTTP、HTML、XML。(与Web有关)
IETF标准和近似标准公布“征求意见稿”(Request for comments, RFC),一经发布就不再改动。可以修改并允许开发的IETF工作文档称为“Internet草案”。
W3C标准有5个基本等级,低高依次为:注解、工作草案、候选推荐、提议推荐、推荐。
7.IP、TCP、UDP:
IP健壮:一个路由器故障,不妨碍网络运行,任意两点之间有多个路由。
IP平台无关:兼容不同类型计算机。
IP不能保证数据包的到达顺序。
TCP:是面向连接的可靠协议。
UDP是不可靠协议:不能保证包的顺序,也不能保证一定到达。
8.IP地址与域名:
IPv4:32 bits
IPv6:128 bits
域名与DNS服务
主机域名转换成IP地址
java.net.InetAddress
IPv4地址概念与地址划分方法
地址长度:32位,用点分十进制(x.x.x.x)表示(x=0~255)
标准IPv4地址的分类:
A类:0 网8 主24 1.0.0.0-127.255.255.255 (注意从1开始)
B类:10 网16 主16 128.0.0.0-191.255.255.255
C类:110 网24 主8 192.0.0.0-223.255.255.255
D类:1110 网32 224.0.0.0.0-239.255.255.255
E类:11110 保留用于实验和将来使用 240.0.0.0-247.255.255.255
特殊IP地址:
网络标识:全0;主机标识:全0;--> 本网络上的本主机
网络标识:全0;主机标识:主机号;--> 本网络上的某个主机
网络标识:网络号;主机标识:全0;--> 指定的一个网络
网络标识:全1;主机标识:全1;--> 只限本网络上进行广播 受限广播地址
网络标识:网络号;主机标识:全1;-->对网络号上所有主机进行广播直接广播地址
网络标识:127;主机标识: ;-->用作本地循环测试
专用IP地址:
IPv6地址格式(128bit)
冒分16进制表示法:
0位压缩需要注意两个问题:
① 在使用零压缩法时,不能将一个位段的有效0压缩掉。
② 双冒号在一个地址中只能出现一次。
IPv4地址嵌入IPv6地址中,格式:x: x: x: x: x: x:d.d.d.d
其前96位采用冒分十六进制表示,后32位地址使用IPv4的点分十进制表示。
例如: ::192.168.0.1 与 ::FFFF:192.168.0.1
IPv6前缀格式
IPv6不支持子网掩码,它只支持前缀长度表示法。
前缀是IPv6地址的一部分,用作IPv6路由或子网标识。
格式: IPv6地址/前缀长度n
9.端口/Port:
解决多个网络应用程序同时工作问题, 应用进程标识的基本方法。
传输层进程寻址:通过TCP/UDP端口号实现。
端口号的分配:
熟知端口号:0-1023
注册端口号:1024-49151
临时端口号:49152-65535
NAT转换:
用户C1向服务器S发起一个HTTP请求,经过NAT转化,服务器收到并作出响应,用户C1收到响应。
10.互联网通信应用程序:
互联网通信应用程序的常见工作模式
(1)C/S工作模式
服务器程序在固定的IP地址和熟知的端口号上一直处于打开状态
客户端之间不能够直接通信
(2)P2P工作模式
P2P网络并不是一个新的网络结构,而是一种新的网络应用模式
11.套接字Socket通信编程:
端口号与IP地址的组合得出一个网络套接字。
端口号被规定为一个16位的整数0-65535。
0~1023被预先定义的服务通信占用(如telnet占用端口23,http占用端口80等)。
除非我们需要访问这些特定的服务,否则,就应该使用1024~65535这些端口中的某一个进行通信,以免发生端口冲突。
套接字连接
套接字连接就是客户端的套接字对象和服务器端的套接字对象通过输入、输出流连接在一起。
套接字连接的基本模式:
(1)服务器建立ServerSocket对象
ServerSocket对象负责等待客户端的请求,进而建立套接字连接。
ServerSocket的构造方法是:ServerSocket(int port)
try
{
ServerSocket waitSocketConnection = new ServerSocket(1880);
}
catch(IOException e){}
当服务器端的ServerSocket对象waitSocketConnection建立后,就可以使用方法accept()接收客户端的套接字连接请求,代码如下所示:
Socket socketAtServer = waitSocketConnection.accept();
所谓“接收”客户的套接字请求,就是accept()方法会返回一个Socket对象(即socketAtServer),称作服务器端的套接字对象。
(2)客户端创建Socket对象
客户端程序可以使用Socket类创建对象,Socket的构造方法是:Socket(String host, int port)
Socket socketAtClient = new Socket(localhost, 1880);
也可以使用Socket类不带参数的构造方法public Socket()。该对象再调用public void connect(InetSocketAddress endpoint) throws IOException请求和参数InetSocketAddress指定地址的套接字建立连接。
客户端建立Socket对象(即socketAtClient)的过程就是向服务器发出套接字连接请求,如果服务器端相应的端口上有套接字对象正在使用accept()方法等待客户,那么双方的套接字对象(即socketAtClient和socketAtServer)就都诞生了。
(3)流连接
客户端和服务器端的Socket对象诞生以后,还必须进行输入、输出流的连接。
服务器端的这个Socket对象(即socketAtServer)使用方法getOutputStream()获得的输出流,将指向客户端Socket对象(即socketAtClient)使用方法getInputStream()获得的那个输入流。
服务器端的这个Socket对象(即socketAtServer)使用方法getInputStream()获得的输入流,将指向客户端Socket对象(即socketAtClient)使用方法getOutputStream()获得的那个输出流。
因此,当服务器向这个输出流写入信息时,客户端通过相应的输入流就能读取,反之亦然。
套接字Socket通信编程例子
import java.io.*;
import java.net.*;
public class Server
{
public static void main(String args[])
{
ServerSocket server=null;
Socket socketAtServer=null;
DataOutputStream out=null;
DataInputStream in=null;
try
{
server=new ServerSocket(4333);
}
catch(IOException e1)
{
System.out.println("ERRO:"+e1);
}
try
{
socketAtServer=server.accept();
in=new DataInputStream(socketAtServer.getInputStream());
out=new DataOutputStream(socketAtServer.getOutputStream());
while(true)
{
int m=0;
m=in.readInt();
out.writeInt(m*2);
System.out.println("Server received:"+m);
Thread.sleep(500);
}
}
catch(IOException e)
{
System.out.println(""+e);
}
catch(InterruptedException e){}
}
}
import java.io.*;
import java.net.*;
public class Client
{
public static void main(String args[])
{
Socket socketAtClient;
DataInputStream in=null;
DataOutputStream out=null;
try
{
socketAtClient=new Socket("localhost",4333);
in = new DataInputStream(socketAtClient.getInputStream());
out = new DataOutputStream(socketAtClient.getOutputStream());
out.writeInt(1);
while(true)
{
int m2=0;
m2=in.readInt();
out.writeInt(m2);
System.out.println("Client received:"+m2);
Thread.sleep(500);
}
}
catch(IOException e)
{
System.out.println("Unable to connect to the server");
}
catch(InterruptedException e){}
}
}
先运行服务器端程序Server.java,再运行客户端程序Client.java
Server received : 1 2 4 8 16...
Client received : 2 4 8 16...
一个服务器端与多个客户端连接:
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.Socket;
/*
处理客户端的连接的线程
*/
public class ClientHandler implements Runnable{
private final Socket clientSocket;//客户端Socket
private final ServerGUI serverGUI;//服务端GUI实例
private PrintWriter clientWriter;//用于向客户端发送消息的写入器
private String clientName;//客户端名称
//构造函数,初始化客户端处理器
public ClientHandler(Socket clientSocket,ServerGUI serverGUI,String clientName){
this.clientSocket = clientSocket;
this.serverGUI = serverGUI;
this.clientName = clientName;
try{
clientWriter = new PrintWriter(clientSocket.getOutputStream(),true);
}catch (IOException e){
e.printStackTrace();
}
}
//向客户端发送消息的方法
public void setToClient(String message){
clientWriter.println(message);
}
//线程运行方法,处理客户端的消息接收
@Override
public void run() {
try{//创建用于读取客户端消息的读取器
BufferedReader reader = new BufferedReader(
new InputStreamReader(clientSocket.getInputStream())
);
String message;//持续监听客户端发来的消息
while((message = reader.readLine()) != null){
//在服务端的窗口中显示客户端发来的消息
serverGUI.logMessage("客户端"+clientName+"发来消息: " + message);
}
reader.close();
clientSocket.close();
}catch (IOException e){
e.printStackTrace();
}
}
}
import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.HashMap;
import java.util.Map;
/*
服务端
*/
public class Server {
private ServerGUI serverGUI;//服务端GUI实例
private Map<String, ClientHandler> clients;//存储客户端处理器的映射
public Server(){
serverGUI = new ServerGUI(this);
clients = new HashMap<>();
}
//启动服务器,监听客户端连接
public void startServer(){
try{//创建服务器的Socket,监听指定端口
ServerSocket serverSocket = new ServerSocket(9999);
serverGUI.logMessage("服务器已启动,等待客户端连接...");
int clientCounter = 0;
while(true){//持续接受客户端连接
Socket clientSocket = serverSocket.accept();
clientCounter++;
String clientName = String.valueOf(clientCounter);
serverGUI.logMessage("客户端"+clientName+"已连接");
//创建一个新线程来处理客户端连接
ClientHandler clientHandler = new ClientHandler(clientSocket,serverGUI,clientName);
clients.put(clientName, clientHandler);
serverGUI.addClientHandler(clientHandler);
new Thread(clientHandler).start();
}
}catch(IOException e){
e.printStackTrace();
}
}
//向所有客户端发送消息
public void sendToAllClients(String message){
serverGUI.logMessage("服务器发送给所有客户端:"+message);
//获取所有客户端处理线程,向每个客户端发送消息
for(ClientHandler clientHandler : serverGUI.getClientHandlers()){
clientHandler.setToClient(message);
}
}
//向特定客户端发送消息
public void sendToOneClient(String clientName, String message) {
if (clients.containsKey(clientName)) {
serverGUI.logMessage("服务器发送给客户端"+clientName+": "+message);
clients.get(clientName).setToClient(message);
} else {
serverGUI.logMessage("客户端 " + clientName + " 不存在或已断开连接");
}
}
public static void main(String[] args){
Server server = new Server();
server.startServer();
}
}
第二讲:网络数据流
1.网络程序的输入输出
程序大都要完成读写数据工作,将数据从一个系统移动到另一个系统。
2.Java I/O流分析
Java程序读写文件时可以使用输入/输出流,简称I/O流。
输入和输出都是从程序的角度来说。
输入流(input stream or input object)的数据来源称作“源”。
程序从输入流中读取“源”中的数据。
输出流(output stream or output object)的指向称作 “目的地”。
程序通过向输出流中写入数据,把信息传递到“ 目的地”。
程序的“源”和“目的地”可以是文件、键盘、鼠标、内存或显示器窗口、网络。
节点流:直接与数据源相连,读入或读出。
读写字节数据的节点流:
FileInputStream/FileOutputStream
PipedInputStream/PipedOutputStream
ByteArrayInputStream/ByteArrayOutputStream
读写字符数据的节点流:
FileReader/FileWriter
PipedReader/PipedWriter
CharArrayReader/CharArrayWriter
直接使用节点流,读写不方便,为了更快的读写文件,才有了处理流。
处理流:与节点流一块使用,在节点流的基础上,再套接一层,套接在节点流上的就是处理流。
快速读写字节数据的处理流:
BufferedInputStream/BufferedOutputStream
SequenceInputStream/SequenceOutputStream
DataInputStream/DataOutputStream
ObjectInputStream/ObjectOutputSteam
快速读写字符数据的处理流:
BufferedReader/BufferedWriter
InputStreamReader/OutputStreamWriter
PrintReader/PrintWriter
根据数据传输特性将流抽象为各种类,方便更直观地进行数据操作。
java.io中有4个重要的abstract class:
InputStream(字节输入流)
OutputStream(字节输出流)
Reader(字符输入流)
Writer(字符输出流)
(字节输入输出流一次读入或输出8位二进制;字符输入输出流一次读入或输出16位二进制)
对管道进行操作:PipedInputStream的一个实例要和PipedOutputStream的一个实例共同使用,共同完成管道的读取写入操作。主要用于线程操作。
字节/字符数组操作:是在内存中开辟了一个字节或字符数组。
ByteArrayInputStream
ByteArrayOutputStream
CharArrayReader
CharArrayWriter
Buffered缓冲流:是带缓冲区的处理流,缓冲区的作用是避免每次和硬盘或网络打交道,提高数据访问或传输的效率。
BufferedInputStream
BufferedOutputStream
BufferedReader
BufferedWriter
转化流:把字节转化成字符。
InputStreamReader
OutputStreamWriter
数据流:可以直接输出float类型或long类型,提高了数据读写的效率。
DataInputStream
DataOutputStream
打印流:是打印输出到控制台。
printStream
printWriter
对象流:把封装的对象直接输出,而不是一个个在转换成字符串再输出。
ObjectInputStream
ObjectOutputStream
使用对象流需要实现Serializable接口,否则会报错。
序列化流:把对象直接转换成二进制,写入介质中。
SequenceInputStream
3.缓冲字符流编程
缓冲字符流BufferedReader类:
BufferedReader的构造方法:BufferedReader(Reader in)
BufferedReader流能够读取文本行,方法是readLine()
文本数据的读取和分析中用的比较多的是FileReader和BufferedReader,达到按行读取的目的。
通过向BufferedReader传递一个Reader对象(如FileReader的对象),来创建一个BufferedReader对象:
FileReader fr = new FileReader("Student.txt");
BufferedReader input = new BufferedReader(fr);
然后,input调用readLine()顺序读取文件Student.txt的一行。
缓冲字符流BufferedWriter类:
类似地,可以将BufferedWriter流和FileWriter流连接在一起,然后使用BufferedWriter流将数据写到目的地:
FileWriter fw = new FileWriter("hello.txt");
BufferedWriter output = new BufferedWriter(fw);
BufferedWritter流调用如下方法,把字符串s 或 s的一部分写入到目的地:
write(String s);write(String s, int off, int len)
public class Example9_5{
public static void main(String args[]){
try{
FileReader fr=new FileReader("input.txt");
BufferedReader input=new BufferedReader(fr);
FileWriter fw = new FileWriter("output.txt");
BufferedWriter output = new BufferedWriter(fw);
String s=null;
int i=0;
while((s=input.readLine())!=null){
i++;
output.write(i+": "+s);
output.newLine();
}
output.flush();output.close();input.close();
fr.close();fw.close();
}catch(IOException e){
System.out.println(e);
}
}
}
4.文件字节流编程
文件字节流FileInputStream类
为了创建FileInputStream类的对象,可以使用下列构造方法:
FileInputStream(String name);FileInputStream(File file)
文件字节流FileOutputStream类
构造方法:FileOutputStream(String name);FileOutputStream(File file)
输出流通过使用write()方法把数据写入输出流到达目的地:
输出流通过使用write()方法把数据写入输出流到达目的地。
public void write(byte b[],int off,int len):从给定字节数组中起始于偏移量off处写len个字节到输出流,参数b是存放了数据的字节。
import java.io.*;
public class Example9_3{
public static void main(String args[]){
File file = new File("hello.txt");
byte b[]="深圳大学".getBytes();//getBytes得到字节数组
try{
FileOutputStream output = new FileOutputStream(file);
output.write(b); // write还可以字节数组
output.close();
FileInputStream input = new FileInputStream(file);
while((n=input.read(b,0,2))!=-1)//最多读2个字节!
{
String str=new String(b,0,n);//转为字符串!
System.out.println(str);
}
}catch(IOException e){
System.out.println(e);
}
}
}
5.数据流编程
数据流DataInputStream类和DataOutputStream类
DataInputStream类创建的对象称为数据输入流
DataOutputStream类创建的对象称为数据输出流
import java.io.*;
public class Example9_8{
public static void main(String args[]){
try{
FileOutputStream fos = new FileOutputStream("jerry.dat");
DataOutputStream output = new DataOutputStream(fos);
output.writeInt(100);
output.writeChars("I am ok");
}
catch(IOException e){}
try{
FileInputStream fis = new FileInputStream("jerry.dat");
DataInputStream input = new DataInputStream(fis);
System.out.println(input.readInt());
char c;
while((c=input.readChar())!='\0') //'\0'表示空字符
System.out.print(c);
}
catch(IOException e){}
}
}
// 100
// I am ok
6.对象流编程
对象流ObjectInputStream类和ObjectOutputStream类
ObjectInputStream类创建的对象被称为对象输入流
ObjectOutputStream类创建的对象被称为对象输出流
对象输出流使用writeObject(Object obj)方法将一个对象obj写入输出流
对象输入流使用readObject()从源中读取一个对象到程序中
一个类如果实现了Serializable接口,那么这个类创建的对象就是序列化的对象(a serializable object)。
当把一个序列化的对象写入到对象输出流时,JVM会自动按着一定格式的文本将对象写入到目的地。
import java.io.*;
class Goods implements Serializable
{
String name = null;
double unitPrice;
Goods(String name, double unitPrice){
this.name=name;
this.unitPrice=unitPrice;
}
public void setUnitPrice(double unitPrice){
this.unitPrice=unitPrice;
}
public double getUnitPrice(){
return unitPrice;
}
public void setName(String name){
this.name=name;
}
public String getName(){
return name;
}
}
public class Example9_9
{
public static void main(String args[])
{
Goods TV1 = new Goods("HaierTV",3468);
try{
FileOutputStream fileOut=new FileOutputStream("a.txt");
ObjectOutputStream objectOut=new ObjectOutputStream(fileOut);
objectOut.writeObject(TV1);
FileInputStream fileIn = new FileInputStream("a.txt");
ObjectInputStream objectIn = new ObjectInputStream(fileIn);
Goods TV2=(Goods)objectIn.readObject();
TV2.setUnitPrice(8888);
TV2.setName("GreatWall");
System.out.printf("\nTv1:%s,%f",TV1.getName(),TV1.getUnitPrice());
System.out.printf("\nTv2:%s,%f",TV2.getName(),TV2.getUnitPrice());
}
catch(Exception event){
System.out.println(event);
}
}
}
//Tv1:HaierTV,3468.000000
//Tv2:GreatWall,8888.000000
7.字节/字符数组流编程
字节数组流
使用字节数组作为流的源和目的地。
字节输入流:ByteArrayInputStream
字节输出流:ByteArrayOutputStream
读写场景:向内存(输出流的缓冲区)写入ASCII表,然后再读出这些字节和字节对应的字符。
import java.io.*;
public class Example9_6
{
public static void main(String args[])
{
int n=-1;
ByteArrayOutputStream output=new ByteArrayOutputStream();
for(int i=0;i<5;i++)
{
output.write('A'+i);
}
ByteArrayInputStream input=new ByteArrayInputStream(output.toByteArray());//返回输出流写入到缓冲区的全部字节
while((n=input.read())!=-1)
{
System.out.println(n+":"+(char)n);
}
}
}
//65:A
//66:B
//67:C
//68:D
//69:E
字符数组流
使用字符数组作为流的源和目的地。
与数组字节流对应的是数组字符流:CharArrayReader;CharArrayWriter
读写场景:将Unicode表中的一些字符写入内存,然后再读出。
import java.io.*;
public class Example9_7{
public static void main(String args[]){
int n=-1;
CharArrayWriter output=new CharArrayWriter();
for(int i=65;i<=69;i++)
{
output.write(i);
}
CharArrayReader output=new CharArrayReader(output.toCharArray());//返回输出流写入到缓冲区的全部字符
try
{
while((n=input.read())!=‐1){
System.out.println(n + ":" +(char)n);
}
}
catch(IOException e){}
}
}
//65:A
//66:B
//67:C
//68:D
//69:E
8.网络数据流编码与安全读写问题
将data.txt文件中保存的信息缓冲输入到程序中
(1)应用场景问题:(串链过滤器编程)
代码方式1:
FileInputSteam fin = new FileInputStream(“data.txt”);
BufferedInputStream bin = new BufferedInputStream(fin);
可以使用fin, bin 访问data.txt
代码方式2:
InputSteam in = new FileInputStream(“data.txt”);
in = new BufferedInputStream(in);
只能使用 in 访问data.txt
代码方式3:过滤器与数据流绑定,不能断开
DataOutputStream dout = new DataOutputStream(new BufferedOutputStream(new FileOutputStream(“data.txt”)))
(2)应用场景问题:数据格式不一致,需编码转换
将JSP或Servlet中的请求数据从UTF8转换为GB2312
byte [] b;
String utf8_value;
/*从HTTP流中取"NAME"的UTF8数据*/
utf8_value = request.getParameter("NAME");
b = utf8_value.getBytes("8859_1"); //中间用ISO-8859-1过渡
String name = new String(b, "GB2312"); //转换成GB2312字符
(3)应用场景问题:安全读取网络数据流编程
在读取数据时检测实际读到的长度,如果没有读完已知长度的数据就应该再次读取,以此循环检测,直到实际读取的长度累加与已知的长度相等。
ServerInputStream inStream=request.getInputStream();//取HTTP请求流
int size=request.getContentLength();//取HTTP请求流长度
byte[] buffer = new byte[size]; //用于缓存每次读取的数据
byte[] in_b = new byte[size]; //用于存放结果的数组
int count =0;
int rbyte =0;
while (count < size) { //循环读取
rbyte=inStream.read(buffer);//使用buffer进行读取缓存,rbyte存实际读取长度
for(int i=0;i;){
in_b[count + i] = buffer[i];
}
count += rbyte;
}
第三讲:线程1
1.网络编程应用场景
FTP应用编程: FTP客户端来一个连接, FTP服务器创建一个进程处理连接。
存在问题: 多用户并发情况下, 多进程易拖跨FTP服务器, 服务器响应性能极差。
Http应用编程:并发多个HTTP连接请求,也会造成web服务器性能下降。如何编程保证web服务器性能上的可靠性?
多进程----多线程------线程池
多线程的局限性:当并发线程数达到4000-20000时,大多数jvm会因内存耗尽而无法承受。
2.应用程序的并发性发展
从多进程到多线程
提升并发量的方法:
过去:以进程为单位
多进程:支持几百个进程;进程池:支持上千个进程
当前:以线程为单位
多线程:支持几千个线程;线程池:支持上万个线程
3.Java多线程编程技术及例子
java多线程编程
(1)创建启动线程对象:
Thread t = new Thread( );
t.start( );
(2)让线程完成的任务放在哪儿?重写run()方法:
编程示范例子:
文件安全算法摘要生成编程:计算多个文件的安全算法(SHA)摘要,对每件文件应用安全散列算法(SHA)计算给出该文件的安全算法摘要。
影响该程序性能的主要因素是?
访问文件的I/O读写;程序的运行速度 >> 文件数据读写速度;等待、程序阻塞, 拖慢程序执行速度;使用多线程编程,缓解程序等待情况。
使用派生Thread方式编写线程
编码例子演示: ch3-1 DigestThread.java
对磁盘中digest(C:\temp\ch3data\digest)目录下的三个文件生成安全算法摘要。
1.txt ,build.xml,2.txt
public class DigestThread extends Thread{
private string filename;
public DigestThread(String filename){
this.filename=filename;
}
@Override
public void run(){
try{
System.out.println("Thread processing file is :");
FileInputStream in =new FileInputStream(filename);
MessageDigest sha=MessageDigest.getInstance("SHA-256");//安全散列算法,注意catch!!
DigestInputStream din=new DigestInputStream(in,sha);
while(din.read()!=-1);//一边读一边哈希
din.close();
byte[] digest=sha.digest();//sha.digest()即为得到的摘要
StringBuilder res=new StringBuilder(filename);
res.append(": ");
res.append(DatatypeConverter.printHexBinary(digest));//转为16进制
System.out.println(res);
}catch(IOException ex){
System.err.println(ex);
}catch(NoSuchAlgorithmException ex){
System.err.println(ex);
}
}
public static void main(String[] args)
{
System.out.println("args is :"+args);
for(String filename:args){
Thread t=new DigestThread(filename);
t.start();
}
}
}
// 运行参数:
//C:\temp\ch3data\digest\1.txt
//C:\temp\ch3data\digest\build.xml
//C:\temp\ch3data\digest\2.txt
使用实现Runnable接口方式编写线程
自定义Runnable线程类(实现Runnable)可以作为其他类的子类。要执行的线程任务放在run()方法中。
Thread t = new Thread(myRunableObject); t.start;
public class DigestRunnable implements Runnable{
private string filename;
public DigestRunnable(String filename){
this.filename=filename;
}
@Override
public void run(){
try{
System.out.println("Thread processing file is :");
FileInputStream in =new FileInputStream(filename);
MessageDigest sha=MessageDigest.getInstance("SHA-256");//安全散列算法,注意catch!!
DigestInputStream din=new DigestInputStream(in,sha);
while(din.read()!=-1);//一边读一边哈希
din.close();
byte[] digest=sha.digest();//sha.digest()即为得到的摘要
StringBuilder res=new StringBuilder(filename);
res.append(": ");
res.append(DatatypeConverter.printHexBinary(digest));//转为16进制
System.out.println(res);
}catch(IOException ex){
System.err.println(ex);
}catch(NoSuchAlgorithmException ex){
System.err.println(ex);
}
}
public static void main(String[] args){
System.out.println("args is :"+args);
for(String filename:args){
DigestRunnable dr=new DigestRunnable(filename);//区别!!
Thread t=new Thread(dr);//区别!!
t.start();
}
}
}
4.从线程返回信息及不稳定性问题
线程的run()方法和start()方法不返回任何值。
直接使用存取方法返回线程任务的结果(有异常风险,程序执行结果不稳定)
public class ReturnDigest implements Runnable{
private string filename;
private byte[] digest;
public DigestRunnable(String filename){
this.filename=filename;
}
@Override
public void run(){
try{
System.out.println("Thread processing file is :");
FileInputStream in =new FileInputStream(filename);
MessageDigest sha=MessageDigest.getInstance("SHA-256");//安全散列算法,注意catch!!
DigestInputStream din=new DigestInputStream(in,sha);
while(din.read()!=-1);//读一整个文件
din.close();
digest=sha.digest();//MessageDigest的digest()方法
}catch(IOException ex){
System.err.println(ex);
}catch(NoSuchAlgorithmException ex){
System.err.println(ex);
}
}
public byte[] getDigest(){
return digest;
}
}
import javax.xml.bind.*;//DatatypeConverter
public class ReturnDigestUserInterface{
public static void main(String[] args){
for(String filename : args){
ReturnDigest dr=new ReturnDigest(filename);
dr.start();
StringBuilder result=new StringBuilder(filename);
result.append(": ");
byte[] digest=dr.getDigest();
result.append(DatatypeConverter.printHexBinary(digest));
System.out.println(result);
}
}
}
程序运行不稳定的原因:
dr.start()启动的计算线程可能在main()方法调用,dr.getDigest()之前结束,也可能还没有结束。
如果还没有结束,dr.getDigest()就会返回null,程序访问digest时就会抛出NullPointerException异常。
竞态条件:从线程返回信息,到底会得到正确的结果还是异常,或者被挂起,取决于程序生成了多少线程、系统的CPU和磁盘的速度、系统使用多少个CPU、JVM为不同线程分配时间所用的算法等因素。这些决定性因素称为竞态条件。
5.轮询法编程及例子
改进的写法:但是主线程占用CPU周期资源忙于轮询,做了大量不需要重的
工作,浪费CPU周期。工作线程可工作的CPU周期很少。
public static void main(String[] args){
ReturnDigest[] digests=new ReturnDigest[args.length];//数组形式,多少个线程多少个摘要
for(int i=0;i<args.length;i++)//注意java中的数组长度不用加括号
{
//计算摘要
digests[i]=new ReturnDigest(args[i]);
digests[i].start();
}
for(int i=0;i<args.length;i++)
{
while(true)//!!
{
//现在显示结果
byte[] digest=digests[i].getDigest();
if(digest!=null){//!!
StringBuilder result=new StringBuilder(filename);
result.append(": ");
byte[] digest=dr.getDigest();
result.append(DatatypeConverter.printHexBinary(digest));
System.out.println(result);
break;
}
}
}
}
6.回调法编程及例子
回调法:让线程主动告诉主程序它何时结束(或能返回信息)。线程通过调用主类(即启动该线程的类)中的一个方法来通知主程序,称为回调。
相比于轮询法不会浪费太多CPU时间。
静态回调:
public class CallbackDigest implements Runnable{
private String filename;
public CallbackDigest(String filename){
this.filename=filename;
}
@Override
public void run(){
try{
FileInputStream in =new FileInputStream(filename);
MessageDigest sha=MessageDigest.getInstance("SHA-256");
DigestInputStream din=new DigestInputStream(in,sha);
while(din.read()!=-1);//读一整个文件
din.close();
CallbackDigestUserInterface.receiveDigest(digest,filename);//调用主类的receiveDigest方法
}catch(IOException ex){
System.err.println(ex);
}catch(NoSuchAlgorithmException ex){
System.err.println(ex);
}
}
}
public class CallbackDigestUserInterface{
public static void receiveDigest(byte[] digest,String name)
{
StringBuilder result=new StringBuilder(filename);
result.append(": ");
result.append(DatatypeConverter.printHexBinary(digest));
System.out.println(result);
}
public static void main(String[] args)
{
for(String filename:args){
CallbackDigest cb=new CallbackDigest(filename);
Thread t=new Thread(cb);
t.start();
}
}
}
实例回调:
实例回调编程方式比静态回调更灵活。可应对多个类的对象实例想得到线程的计算结果的复杂场景。
public class InstanceCallbackDigest implements Runnable{
private String filename;
private InstanceCallbackDigestUserInterface callback;
public InstanceCallbackDigest(String filename,InstanceCallbackDigestUserInterface callback){
this.filename=filename;
this.callback=callback;//实例回调
}
@Override
public void run(){
try{
FileInputStream in =new FileInputStream(filename);
MessageDigest sha=MessageDigest.getInstance("SHA-256");
DigestInputStream din=new DigestInputStream(in,sha);
while(din.read()!=-1);//读一整个文件
din.close();
byte[] digest=sha.digest();
callback.receiveDigest(digest);//回调
}catch(IOException ex){
System.err.println(ex);
}catch(NoSuchAlgorithmException ex){
System.err.println(ex);
}
}
}
public class InstanceCallbackDigestUserInterface{
private String filename;
private byte[] digest;
public InstanceCallbackDigestUserInterface(String filename)
{
this.filename=filename;
}
public void calculateDigest()
{
InstanceCallbackDigestUserInterface cb=new InstanceCallbackDigestUserInterface(filename,this);
Thread t=new Thread(cb);
t.start();
}
@Override
public String toString()
{
String result=filename+": ";
if(digest!=null){
result+=DatatypeConverter.printHexBinary(digest);
}else{
result+="digest not available";
}
return result;
}
public static void receiveDigest(byte[] digest,String name)
{
StringBuilder result=new StringBuilder(filename);
result.append(": ");
result.append(DatatypeConverter.printHexBinary(digest));
System.out.println(result);
}
public static void main(String[] args)
{
for(String filename:args){
InstanceCallbackDigestUserInterface d=new InstanceCallbackDigestUserInterface(filename);//实例
d.calculateDigest();
}
}
}
7.观察者设计模式
建立一种对象与对象之间的依赖关系,一个对象发生改变时将自动通知其他对象,其他对象将相应做出反应。
发生改变的对象称为观察目标。被通知的对象称为观察者,一个观察目标可以对应多个观察者,而且这些观察者之间没有相互联系,可以根据需要增加和删除观察者,使得系统更易于扩展。
第四讲:线程2
1.多线程与线程池编程技术
多线程编程技术的恰当应用:
优点:网络程序,尤其是I/O受限或CPU受限的网络程序,引入多线程,提升并发性能,提高程序性能。
缺点:线程本身的创建、切换、清理等会耗费性能资源,对程序性能产生负面影响。
合理的度:线程的数目适中。一旦已生成足够多的线程来使用计算机所有可用的空闲时间,再生成更多线程只会将MIPS和内存浪费在线程管理上。
传统多线程处理方式的缺点:
传统多线程处理方式:使用new Thread(...).start()的方法
问题:
( 1)开销大。对于JVM来说,每次新建线程和销毁线程都会有很大的开销。机器的MIPS和内存浪费在线程管理上。
( 2)线程缺乏管理。没有一个池来限制线程的数量,如果并发量很高,会创建很多的线程,而且线程之间可能会有相互竞争,这将会过多得占用系统资源,增加系统资源的消耗量。而且线程数量超过系统负荷,容易导致系统不稳定。
2.Executor并发编程框架
Jdk1.5以上,java.util.concurrent.*中提供了Executor框架封装和隐藏多线程异步控制细节,使回调更易处理。
Executor框架(并发编程框架):是指jdk并发库中与Executor相关的功能类,包括线程池、Executor、Executors、ExecutorService、Future、Callable等。
编程人员可以创建多个不同线程,并能按期望的顺序得到需要的答案。
线程池(Thread Pool)
作用:当需要限制在应用程序中同一时刻运行的线程数时,可使用线程池编程。线程池作用就是限制系统中执行线程的数量
原理:把并发执行的任务提交给一个线程池(不用为每个并发执行的任务都启动一个新的线程)。一旦池里有空闲的线程,就会分配给任务一个线程执行。在线程池的内部,任务被插入一个阻塞队列(Blocking Queue ),线程池里的线程会去取这个队列里的任务。当一个新任务插入队列时,一个空闲线程就会成功地从队列中取出任务并且执行它。
线程池的基本思想:是一种对象池的思想,开辟一块内存空间,里面存放了众多(未死亡)的线程,池中线程执行调度由池管理器来处理。当有线程任务时,从池中取一个,执行完成后线程对象归池,这样可以避免反复创建线程对象所带来的性能开销,节省了系统的资源。
多线程服务器的编程实现常用到线程池。每个通过网络到达服务器的连接都被包装成一个任务并且提交给线程池。线程池的线程会并发的处理连接上的请求。
Java5以上支持线程池编程。
线程池方式的优点
复用线程。通过复用创建的了的线程,减少了线程的创建、消亡的开销,每个工作线程都可以被重复利用,可执行多个任务。
有效控制并发线程数:可以根据系统的承受能力,调整线程池中工作线线程的数目,防止因为消耗过多的内存,而把服务器累趴下(每个线程需要大约1MB内存,线程开的越多,消耗的内存也就越大,最后死机) 。
提供了更简单灵活的线程管理。可以提供定时执行、定期执行、单线程、可变线程数等多种线程使用功能。
Executor框架
Executor框架类之间的关系。
在Executor框架中,使用执行器(Exectuor)来管理Thread对象,从而简化了并发编程。
(1)Callable接口类似于Runnable,为那些其实例可能被另一个线程执行的类设计的。提 供 一 个 call()方 法作 为 线程的执行体。call()方法比run()方法更加强大。有 返 回 值,可 以 抛 出 异 常。当 你 提 交 一 个 Callable 对 象 给 线 程 池 时,将 得 到 一 个Future 对 象,并 且 它 和 你 传 入 的 Callable 示 例 有 相 同泛 型。
(2)Future接口用来对具体的Runnable或者Callable任务的执行结果进行取消、查询是否完成、获取结果。必要时可以通过get()方法获取执行结果,get()方法会阻塞直到任务返回结果。
(3)Executor接口(执行器)将任务的提交与任务的执行分离,定义了一个接收Runnable对象的方法execute。是Executor框架中最基础的一个接口,类似于集合中的Collection接口。
(4)ExecutorService类是Executor的子类接口,定义了终止任务、提交任务、跟踪任务返回结果等方法(API)用于操作Executor。一个ExecutorService关闭之后线程池将不能再接收任何任务。对于不再使用的ExecutorService,应该将其关闭以释放资源。是线程池的实现。
(5)Executors类主要用于提供线程池相关的操作,提供了一系列工厂方法用于创建线程池,返回的线程池都实现了ExecutorService接口。
(6)CompletionService和ExecutorService的主要的区别在于submit的task不一定是按照加入自己维护的list顺序完成的,其实现是维护一个保存Future对象的BlockingQueue,只有当这个Future对象状态是结束的时候,才会加入到这个Queue中。
3.使用Executor的并发程序结构设计
异步计算程序结构设计模型:
多线程编程时,主线程要想获得工作线程(异步计算线程)的结果是比较麻烦的,需要设计特殊的程序结构,比较繁琐而且容易出错。使用Executor框架中的Future来跟踪异步计算的结果,设计良好的异步计算程序结构模型。
根据分而治之的思想,把异步计算的线程按照职责分为3类:
(1)异步计算的发起线程(控制线程):负责异步计算任务的分解和发起,把分解好的任务交给异步计算的work线程去执行,发起异步计算后,发起线程可以获得Futrue的集合,从而可以跟踪异步计算结果。
(2)异步计算work线程:负责具体的计算任务。
(3)异步计算结果收集线程:从发起线程那里获得Future的集合,并负责监控Future的状态,根据Future的状态来处理异步计算的结果。
异步计算程序结构设例子:
使用Executor框架多线程编程查找一个很大的数字数组中的最大值。
主要使用Future、 Callable和Executor类。
import java.util.concurrent.Callable;
class FindMaxTask implements Callable<Integer>{
private int[] data;
private int start;
private int end;
FindMaxTask(int[] data,int start,int end)
{
this.data=data;
this.start=start;
this.end=end;
}
public Integer call()
{
int max=Integer.MIN_VALUE;
for(int i=start;i<end;i++)
{
if(data[i]>max)max=data[i];
}
return max;
}
}
public class MultithreadedMaxFinder{
public static int max(int[] data)throws InterruptedException,ExecutionException{
if(data.length==1)
{
return data[0];
}else if(data.length==0)
{
throw new IllegalArgumentException();
}
//把任务分成两半,两个线程
FindMaxTask task1=new FindMaxTask(data,0,data.length/2);
FindMaxTask task2=new FindMaxTask(data,data.length/2,data.length);
//启动两个线程
ExecutorService service=Executors.newFixedThreadPool(2);
Future<Integer> future1=service.submit(task1);//得到结果
Future<Integer> future2=service.submit(task2);//得到结果
return Math.max(future1.get(),future2.get());//返回结果取Max
}
}
public class StartClass{
public static void main(String[] args){
int[] num=new int[1000];
//生成任意1000整数,0-999之间
for(int i=0;i<num.length;i++)
{
num[i]=(int)(Math.random()*1000);
}
//输出数组中的数
for(int j=0;j<num.length;j++)
{
System.out.println(num[j]);
}
try{
int maxNum=MultithreadedMaxFinder.max(num);
System.out.println("找出的最大数是:"+maxNum);
}catch(Exception ex){
System.err.println(ex);
}
}
}
4.文件压缩并发编程例子
对目录中的每一个文件完成gzip压缩。
应用特点:大量I/O操作。
应用特点:数据压缩是CPU密集度高的操作。
不要同时运行太多线程。
使用线程池编程。
程序设计思想:
public class GZipAllFiles{
public final static THREAD_COUNT=4;
public static void main(String[] args){
ExecutorService pool = Executors.newFixedThreadPool(THREAD_COUNT);
for(String filename:args)
{
File f=new File(filename);
if(f.exists()){
if(f.isDirectory()){
File[] files=f.listFiles();//列出文件
for(int i=0;i<files.length;i++)
{
if(!files[i].isDirectory()){
Runnable task=new GZipARunnable(files[i]);
pool.submit(task);
}
}
}
}
}
pool.shutdown();//不会中止等待的工作,只是通知线程池没有新的工作线程任务需要增加到等待队列,池中所有的任务完成即可关闭。
}
}
import java.util.zip.*;
public class GZipARunnable implements Runnable{
private final File input;//文件输入
public GZipARunnable(File input){
this.input=input;
}
@Override
public void run(){
if(!input.getName.endsWith(".gz")){
File output=new File(input.getParent(),input.getName()+".gz");
if(!output.exists()){ // 没有压缩过的才压缩
try(
InputStream in = new BufferedInputStream(new FileInputStream(input));
OutputStream out=new BufferedOutputStream(
new GZIPOutputStream(
new FileOutputStream(output)));
){ // 括号内要使用冒号
int b;
while((b=in.read())!=-1)out.write(b);
out.flush();
}catch(IOException ex){
System.err.println(ex);
}
}
}
}
}
5.同步块编程技术及例子
多线程访问同一资源问题
多线程共享的资源有内存、文件句柄、socket等。
多线程共享资源,若不同时使用共享的资源则可以提高程序效率。
若同时使用共享的资源,必须互斥共享。同一时刻只允许一个线程使用,其它线程等待,否则资源会被破坏,程序可能会出现混乱结果。
某一时刻,线程1访问,线程2、3、4等待。
编程时:必须能够指定一个共享资源(若干条语句)只能由一个线程独占访问。
同步块编程技术
将需要同步的对象和需要独占访问的语句用synchronized块围起来。以作同步指示。
需独占访问的语句:
System.out.print( input + “:” );
System.out.print(DatatypeConverter.printHexBinary(digest));
System.out.println( );
加上同步块:
Synchronized ( System.out) {
System.out.print( input + “:” );
System.out.print(DatatypeConverter.printHexBinary(digest));
System.out.println( );
}
同步块编程技术应用例子
Web服务器上日志文件的多线程访问
多个线程会使用LogFile对象的引用获得当前日期和时间,写入到日志文件中。如何编程处理,避免写入混乱?
public class LogFile{
private Writer out;
public LogFile(File f)throws IOException{
FileWriter fw=new FileWriter(f);
this.out=new BufferedWriter(fw);
}
//无同步处理
public void writeEntry(String message)throws IOException{
Date d=new Date();
out.write(d.toString());
out.write('\t');
out.write(message);
out.write("\r\n");
}
//同步处理方法1:选择对Writer对象out同步
public void writeEntry(String message)throws IOException{
synchronized(out){
Date d=new Date();
out.write(d.toString());
out.write('\t');
out.write(message);
out.write("\r\n");
}
}
//同步处理方法2:选择对LogFile对象本身同步
public void writeEntry(String message)throws IOException{
synchronized(this){
Date d=new Date();
out.write(d.toString());
out.write('\t');
out.write(message);
out.write("\r\n");
}
}
//同步处理方法3:选择使用synchronized修饰符对对象本身同步整个方法体
public synchronized void writeEntry(String message)throws IOException{
Date d=new Date();
out.write(d.toString());
out.write('\t');
out.write(message);
out.write("\r\n");
}
public void close() throws IOException{
out.flush();
out.close();
}
}
同步块编程技术可能引起的问题
1.可能使VM的性能严重下降,代码执行速度降低三分之一或更多。
2.大大增加了死锁的可能性。
3.并不是对象本身需要防止同时修改或访问,如果只是对该方法所属类的实例进行同步,可能并不能保护真正需要保护的对象。
造成的问题:目标是为了避免4个线程同时使用out对象写入日志文件,但如果其他类有与LogFile完全无关的out的引用,这些写入会失败。
同步编程技术的替换技术
使用同步编程技术解决因线程调度可能引起不一致问题,又会引入新的问题,称为同步问题。
思考:有哪些替换编程技术,可以解决多线程资源共享可能造成的不一致问题,又能避免使用同步而引起同步问题?
方法1:尽量使用局部变量而不是字段
方法2:基本类型方法参数,构造函数通常都是线程安全的
方法3:将非线线程安全类用作为线程安全的类的一个私有字段
线程调度
多线程运行程序要考虑线程调度问题。
重要的线程能够得到CPU时间来运行,更重要的线程能够得到更多的时间,现在能够以合理的顺序执行。
优先级
在Java中,每个线程都有一个优先级,指定为一个从0~10的整数.虚拟机优先调度优先级高的线程执行。10是最高优先级,0是最低,默认优先级为5,通常我们编写程序中的线程都是默认优先级。Thread.MIN_PRIORITY = 1,Thread.NORM_PRIORITY = 5, Thread.MAX_PRIORITY = 10。
线程调度器
每个虚拟机都有一个线程调度器,确定在给定时刻运行哪个线程。
线程调度方式:抢占式和协作式。
抢占式线程调度器定期强行中断当前正在执行的线程,将CPU控制权交给另外的线程。
协作式线程调度器在将CPU控制权交给其他线程前会等待正在运行的线程自己暂停。
在编写Java线程程序实现run()方法时,要确保线程有可以暂停的条件,让其他线程有机会运行。
可以使一个线程暂停的方式有:对I/O阻塞、 对同步对象阻塞、放弃、休眠、连接另一个线程、等待一个对象、结束、可以被更高优先级线程抢占 等。
线程死锁:如果两个线程需要独占访问同样的一个资源集,而每个线程分别有这些资源的不同子集的锁,就会发生死锁。
线程饥饿:在线程调度运行时,有的线程一直得不到CPU时间片,一直处于等待状态,无法运行任务。
6.连接线程编程技术及例子
编程问题场景:计算多个文件安全算法摘要并显示。
使用存取方法的多线程程序设计:
使用存取方法获得计算线程的输出,存在问题,main()方法的速度会超过生成结果的线程,会出现空指针异常。
使用连接线程join
//使用连接生成所需结果的线程,避免竞态条件
public class JoinDigestUserInterface{
public static void main(String[] args){
ReturnDigest[] digestThreads=new ReturnDigest[args.length];//数组形式,多少个线程多少个摘要
for(int i=0;i<args.length;i++)//注意java中的数组长度不用加括号
{
//计算摘要
digestThreads[i]=new ReturnDigest(args[i]);
digestThreads[i].start();
}
for(int i=0;i<args.length;i++)
{
try{
digestThreads[i].join();
StringBuilder result=new StringBuilder(filename);
result.append(": ");
byte[] digest=digestThreads[i].getDigest();
result.append(DatatypeConverter.printHexBinary(digest));
System.out.println(result);
}catch(InterruptedException ex){
System.out.println("Thread Interrupter before completion");
}
}
}
}
连接线程编程技术相关概念
通过调用join( )方法,允许一个线程在继续执行前等待另一个线程结束。
调用join()方法的线程是连接线程(join thread),等待被连接的线程(joined thread)(join方法属于被连接的线程)。
通过调用join( )方法,允许一个线程在继续执行前等待另一个线程结束。 连接线程要等待多久?
取决于join( ) 的参数:
join( ) ----无限等待被连接的线程结束。
join( long milliseconds )---- 等待milliseconds微秒,然后继续执行。
join( long milliseconds, int nanoseconds) ---等待nanoseconds毫秒或milliseconds微秒,然后继续执行。
第五-六讲:Internet地址
1.Internet地址概念
Internet索引(IP地址)
所有连入Internet的终端设备(包括计算机、PDA、打印机以及其他的电子设备)都有一个唯一的索引,这个索引被称为IP地址。在Internet上的IP地址大多由4个字节组成,这种IP地址叫做IPv4。除了这种由四个字节组成的IP,在Internet上还存在一种IP,这种IP由16个字节组成,叫做IPv6 。 IPv4和IPv6后面的数字是Internet协议(Internet Protocol,IP)的版本号。
2.DNS及与IP地址关系
IPV4与IPV6混合网络
在IPv4和IPv6混合的网络中,IPv6地址的后四个字节可以被写成IPv4的地址格式。
如FEDC:BA98:7654:3210:FEDC:BA98:7654:3210可以写成FEDC:BA98:7654:3210:FEDC:BA98:118.84.50.16。
当访问网络资源的计算机使用的是IPv4的地址时,系统会自动使用IPv6的后四个字节作为IPv4的地址。
DNS
无论是IPv4地址,还是IPv6地址,都是很难记忆的。
为了使这些地址便于记忆,Internet的设计师们发明了DNS。
DNS将IP地址和域名(一个容易记忆的字符串)联系在一起 。
当计算机通过域名访问Internet资源时,系统首先通过DNS得到域名对应的IP地址,再通过IP地址访问Internet资源。在这个过程中,IP地址对用户是完全透明的。如果一个域名对应了多个IP地址,DNS从这些IP地址中随机选。
域名系统(Domain Name System, DNS)
DNS将易于记忆的域名与IP地址联系起来。
关系:一台主机可能有多个IP地址;一个IP地址可能被多个域名指向;一个域名可能指向多个。
3.InetAddress类及应用编程
InetAddress类是Java对IP地址的封装,用于描述IP地址的类。
它包括一个主机名和一个IP地址。
它在java.net包中(rt.jar),此包中许多类都使用到了InetAddress,包括ServerSocket,Socket,DatagramSocket等。
在Java中分别用Inet4Address和Inet6Address类来描述IPv4和IPv6的地址。这两个类都是InetAddress的子类。
4.InetAddress对象与程序安全性问题
创建InetAddress对象
InetAddress没有public的构造方法,要想创建InetAddress对象,必须得依靠它的四个静态方法:
InetAddress可以通过getLocalHost方法得到本机的InetAddress对象。 也可以通过getByName、getAllByName和getByAddress得到远程主机的InetAddress对象。
创建方法:
Static InetAddress getByName(String s)
Static InetAddress[] getAllByName(String s)
Static InetAddress getLocalHost()
getLocalHost方法
使用getLocalHost可以得到描述本机IP的InetAddress对象。这个方法的定义:public static InetAddress getLocalHost() throws UnknownHostException
在调用这个方法的程序中捕捉或抛出UnknownHostException这个异常。
使用getLocalHost来得到本机的计算机名和IP:
import java.net.*;
public class MyInetAddress1 {
public static void main(String[] args) throws Exception{
InetAddress localAddress = InetAddress.getLocalHost();
System.out.println(localAddress);
}
}
//运行结果例如:mfq-THINK/172.31.75.47
当本机绑定了多个IP时,getLocalHost只返回第一个IP。如果想返回本机全部的IP,可以使用getAllByName方法。
getByName方法
可以通过该方法指定域名从DNS中查得与域名相对应的IP地址。
getByName一个String类型参数,可以通过这个参数指定远程主机的域名,它的定义如下:
public static InetAddress getByName(String host) throws UnknownHostException
如果host所指的域名对应多个IP,getByName返回第一个IP。
如果本机名已知,可以使用getByName方法来代替getLocalHost。
当host的值是localhost时,返回的IP一般是127.0.0.1。
如果host是不存在的域名,getByName将抛出UnknownHostException异常,如果host是IP地址,无论这个IP地址是否存在,getByName方法都会返回这个IP地址(因此getByName并不验证IP地址的正确性)。
getByName方法会与本地DNS服务器建立一个连接,根据提供的名字查找到对应的IP。首先会在本机缓存中查找,如果能找到,就不会建立网络连接。
import java.net.*;
public class MyInetAddress2 {
public static void main(String[] args)throws Exception {
if (args.length == 0)
return;
String host = args[0];
InetAddress address = InetAddress.getByName(host);
System.out.println(address);
}
}
对于本机来说,除了可以使用本机名或localhost外,还可以在hosts文件中对本机做“IP/域名”映射(在Windows操作系统下)。这个文件在C:\Windows\System32\drivers\etc中。打开hosts,在最后加一行如下所示的字符串:
192.168.18.100 www.mysite.com
测试:本机域名www.mysite.com
运行结果:www.mysite.com/192.168.18.100
创建新的InetAddress对象
显示www.oreilly.com地址:为该域名对应的主机创建一个InetAddress对象
import java.net.*;
public class OReillyByName{
public static void main(String[] args){
try{
InetAddress address=InetAddress.getByName("www.oreilly.com");
System.out.println(address);
}catch(UnknownHostException ex){
System.out.println("Could not find www.oreilly.com");
}
}
}
getAllByName方法
使用getAllByName方法可以从DNS上得到域名对应的所有的IP。这个方法返回一个InetAddress类型的数组。这个方法的定义如下:
public static InetAddress[] getAllByName(String host) throws UnknownHostException
与getByName方法一样,当host不存在时,getAllByName也会抛出UnknowHostException异常,getAllByName也不会验证IP地址是否存在。
public class MyInetAddress3 {
public static void main(String[] args) throws Exception{
if (args.length == 0)
return;
String host = args[0];
InetAddress addresses[]=InetAddress.getAllByName(host);
for (InetAddress address : addresses)
System.out.println(address);
}
}
getByAddress方法
这个方法必须通过IP地址来创建InetAddress对象,而且IP地址必须是byte数组形式。getByAddress方法有两个重载形式,定义如下:
public static InetAddress getByAddress(byte[] addr) throws UnknownHostException
public static InetAddress getByAddress(String host, byte[] addr) throws UnknownHostException
第一个重载形式只需要传递byte数组形式的IP地址,getByAddress方法并不验证这个IP地址是否存在,只是简单地创建一个InetAddress对象。
addr数组的长度必须是4 (IPv4)或16(IPv6),如果是其他长度的byte数组,getByAddress将抛出一个UnknownHostException异常。
第二个重载形式多了一个host,这个host和getByName、getAllByName方法中的host的意义不同,getByAddress方法并不使用host在DNS上查找IP地址,这个host只是一个用于表示addr的别名。
import java.net.*;
public class MyInetAddress4 {
public static void main(String[] args) throws Exception{
byte ip[] = new byte[] { (byte) 141, (byte) 146, 8 , 66};//可以使用String.getBytes();
InetAddress address1 = InetAddress.getByAddress(ip);
InetAddress address2 = InetAddress.getByAddress("Oracle官方网站", ip);
System.out.println(address1);
System.out.println(address2);
}
}
//141.146.8.66
//Oracle官方网站/141.146.8.66
程序安全性问题
根据主机名创建一个InetAddress对象不安全。
InetAddress.getByName()/getAllByName()/getLocalHost()都存在此安全隐患。
因为这些创建方法需要一个DNS查找,任意的DNS查找会打开一个隐藏的通道,造成信息泄露。
在程序中常需先测试一个主机是否支持DNS解析,使用SecurityManager的方法:public void checkConnect(String hostname, int port)
port参数为-1,该方法能检查能否调用DNS解析指定的hostname。
为什么不直接通过IP访问网站
当在浏览器地址栏中再输入http://www.126.com可以访问,但输入IP http://125.90.204.122却不可以,这并不是客户端的问题,而是服务端对此做了限制。
在HTTP协议的请求头有一个Host字段,一般通过http://www.126.com访问服务器时,Host的值就是www.126.com 。如果是http://125.90.204.122 ,那么Host的值就是125.90.204.122 。www.126.com的服务器通过检测Host字段防止客户端直接使用IP进行访问。
目前有很多网站,如www.sina.com.cn,www.163.com都是这样做的。有一些网站虽然未限制用IP地址来访问,但在使用IP地址访问网站时,却将IP地址又重定位到相应的域名上。如输入http://210.39.3.164会重定位到http://www.szu.edu.cn/szu.asp 上。
5.DNS缓存应用编程
在DNS服务器上查找域名操作非常昂贵:因在通过DNS查找域名的过程中,可能会经过多台中间DNS服务器才能找到指定的域名。
Java提供DNS缓存解决此问题。
当InetAddress类第一次使用某个域名(如www.szu.edu.cn)创建InetAddress对象后,JVM就会将这个域名和它 从DNS上获得的信息(如IP地址)都保存在DNS缓存中。
当下一次InetAddress类再使用这个域名时,就直接从DNS缓存里获得所需的信息,而无需再访问DNS服务器。
DNS缓存在默认时将永远保留曾经访问过的域名信息,但我们可以修改这个默认值。
DNS查询的开销较大(可能几秒)。
InetAddress会缓存查找的结果。默认只缓存10秒。
本机、本地DNS、其他DNS也会缓存查询,Java无法控制这些缓存。
修改域名指向可能需要几个小时才能生效。
按IP地址查找getByName("x.x.x.x"),不检查DNS,直接创建对象。
DNS查找结果在java缓存中的保留时间
通过java.security.Security.setProperty方法设置系统属性networkaddress.cache.ttl的值(单位:秒)。如下面的代码将指定成功的DNS查找结果在Java缓存中保留的时间为10秒:
java.security.Security.setProperty("networkaddress.cache.ttl", 10);
设置系统属性networkaddress.cache.negative.ttl的值,指定不成功的DNS查找结果在Java缓存中保留的时间(秒数)。如果将networkaddress.cache.ttl属性值设为-1,那么DNS在InetAddress类中的缓存数据将永不过期。
import java.net.*;
public class MyDNS{
public static void main(String[] args)throw Exception{
// args[0]: 本机名 args[1]:缓冲时间
if (args.length < 2)
return;
java.secutity.Security.setProperty("networkaddress.cache.ttl",args[1]);//缓冲超时参数
long time=System.currentTimeMillis();
InetAddress addresses1[]=InetAddress.getAllByName(args[0]);
System.out.println("addresses1: "+ String.valueOf(System.currentTimeMillis() ‐ time)+"毫秒");
for (InetAddress address : addresses1)
System.out.println(address);
System.out.print("按任意键继续 …");
System.in.read();//暂停程序
time = System.currentTimeMillis();
InetAddress addresses2[]=InetAddress.getAllByName(args[0]);//使用同一个域名再建立一个InetAddress数组
System.out.println("addresses2: "+ String.valueOf(System.currentTimeMillis() ‐ time) +"毫秒");
for (InetAddress address : addresses2)
System.out.println(address);
}
}
当用户等待一段时间后,可以按任意键继续,并使用同一个域名(args[0])再建立一个InetAddress数组。如果用户等待的这段时间比DNS缓存超时小,那么无论情况如何变化,addresses2和addresses1数组中的元素是一样的,并且创建addresses2数组所花费的时间一般为0毫秒(小于1毫秒后,Java无法获得更精确的时间)。
修改DNS缓存编程例子
测试1:执行如下命令(将DNS缓存超时设为5秒)java MyDNS www.126.com 5
测试1可能出现两个运行结果。如果在出现“按任意键继续…”后,在5秒之内按任意键继续后,就会得到运行结果1,从这个结果可以看出,addresses2所用的时间为0毫秒,也就是说,addresses2并未真正访问DNS服务器,而是直接从内存中的DNS缓存得到的数据。当在5秒后按任意键继续后,就会得到运行结果2,这时,内存中的DNS缓存中的数据已经释放,所以addresses2还得再访问DNS服务器,因此,addresses2的时间是1毫秒。
测试2:执行如下命令( mfq-THINK为本机的计算机名,DNS缓存超时设为永不过期[-1]): java MyDNS mfq-THINK -1
将DNS缓存设为永不过期后,addresses2是从DNS缓存中得到数据。无论过多少时间,按任意键后,addresses2仍然得到了与address1一样的IP地址,而且addresses2的时间是0毫秒。如果域名在DNS服务器上不存在,那么客户端在进行一段时间的尝试后(平均为 5秒),就会抛出一个UnknownHostException异常。为了让下一次访问这个域名时不再等待,DNS缓存将这个错误信息也保存了起来。只有第一次访问错误域名时才进行5 秒左右的尝试,以后再访问这个域名时将直接抛出UnknownHostException异常,而无需再等待5秒钟。
域名错误信息的保留时间设置
访问域名失败的原因可能是这个域名真的不存在,也可能是因为DNS服务器或是其他的硬件或软件的临时故障,因此,一般不能将这个域名错误信息一直保留。
可以通过networkaddress.cache.negative.ttl属性设置保留这些信息的时间,这个属性的默认值是10秒。
也可以通过java.security.Security.setProperty方法或java.security文件来设置。
下面的代码演示了networkaddress.cache.negative.ttl属性的用法。
程序分别测试了address1和address2访问www.123456.com (这是个不存在的域名)后,用了多长时间抛出UnknownHostException异常
import java.net.*;
public class MyDNS1 {
public static void main(String[] args) throws Exception {
java.security.Security.setProperty("networkaddress.cache.negative.ttl","5");//将属性值设计为5秒
long time = 0;
try
{
time = System.currentTimeMillis();
InetAddress.getByName("www.123456.com");
}
catch (Exception e)
{
System.out.println("www.123456.com不存在! address1:"+ String.valueOf(System.currentTimeMillis() ‐ time)+"毫秒");
}
//Thread.sleep(6000); // 延迟6秒
try
{
time = System.currentTimeMillis();
InetAddress.getByName("www.123456.com");
}
catch (Exception e)
{
System.out.println("www.123456.com不存在! address2:"+ String.valueOf(System.currentTimeMillis() ‐ time) +"毫秒");
}
}
}
域名错误信息的保留时间设置例子
测试1:运行结果
分析: address1用了16毫秒抛出了异常,address2用了0毫秒就抛出了异常。可以断定address2是从DNS缓存里获得了域名www123456.com不可访问的信息,所以就直接抛出了UnknowHostException异常。
测试2:如果将上面代码中的延迟代码的注释去掉,也就是没有延迟6秒,那么可能得到如下的运行结果:
分析:在第6秒时,DNS缓存中的数据已经被释放,因此,address2仍需要访问DNS服务器才能知道www.123456com是不可访问的域名。
域名错误信息的保留时间设置原则
总结注意事项:
可以根据实际情况设置networkaddress.cache.ttl属性的值。一般将这个属性的值设为-1。但如果访问的是动态映射的域名(如使用动态域名服务将域名映射成ADSL的动态IP), 就可能产生IP地址变化后,客户端得到的还是原来的IP地址的情况。
在设置networkaddress.cache.negative.ttl属性值时最好不要将它设为-1,否则如果一个域名因为暂时的故障而无法访问,那么程序再次访问这个域名时,即使这个域名恢复正常,程序也无法再访问这个域名了。除非重新运行程序。
6.InetAddress的主机名和域名获取方法及应用编程
四个获取方法可以将主机名作为字符串返回,将IP地址返回为字符串和字节数组。
String getHostName():返回主机名和IP。
String getCanonicalHostName():查询DNS,再返回主机名和IP。
Byte[] getAddress():返回IP,字节类型,IPv4或IPv6。
String getHostAddress():返回IP,字符串类型,IPv4或IPv6。
Inet4Address/Inet6Address
public final Inet4Address extends InetAddress
public final Inet6Address extends InetAddress
一般用不到,可用getAddress()返回的数组长度判断IP地址类型
public class AddressTests{
public static int getVersion(InetAddress ia){
byte[] address=ia.getAddress();
if(address.length==4)return 4;
else if(address.length==16)return 6;
else return -1;
}
}
使用getHostName方法获得域名
getHostName方法可以得到远程主机的域名,也可以得到本机名,定义如下:
public String getHostName()
三种创建InetAddress对象的方式,getHostName返回的值是不同的。
(1)使用getLocalHost方法创建InetAddress对象:getHostName返回的是本机名。
InetAddress address = InetAddress.getLocalHost();
System.out.println(address.getHostName()); // 输出本机名
(2)使用域名创建InetAddress对象:用域名作为getByName和getAllByName方法的参数调用这两个方法后,系统会自动记住这个域名。当调用getHostName方法时,就无需再访问DNS服务器,而是直接将这个域名返回。
InetAddress address = InetAddress.getByName("www.szu.edu.cn");
System.out.println(address.getHostName()); // 无需访问DNS服务器,直接返回域名
(3)使用IP地址创建InetAddress对象:使用IP地址创建InetAddress对象时(getByName、getAllByName和getByAddress方法都可以通过IP地址创建InetAddress对象),并不需要访问DNS服务器。通过DNS服务器查找域名的工作就由getHostName方法来完成。如果这个IP地址不存在或DNS服务器不允许进行IP地址和域名的映射,getHostName方法就直接返回这个IP地址。
InetAddress address = InetAddress.getByName("141.146.8.66");
System.out.println(address.getHostName()); // 需要访问DNS服务器才能得到域名
InetAddress address = InetAddress.getByName("1.2.3.4"); // IP地址不存在
System.out.println(address.getHostName()); // 直接返回IP地址
总结:三种方式,只有通过使用IP地址创建的InetAddress对象调getHostName方法时才访问DNS服务器。在其他情况,getHostName方法并不会访问DNS服务器,而是直接将域名或本机名返回。
在不同情况下如何使用getHostName方法,并计算了各种情况所需的毫秒数:
import java.net.*;
public class DomainName {
public static void main(String[] args) throws Exception{
long time = 0;
//得到本机名
InetAddress address1=InetAddress.getLocalHost();
System.out.println("本机名:"+address1.getHostName());
//直接返回域名
InetAddress address2=InetAddress.getByName("www.szu.edu.com");
time=System.currentTimeMillis();
System.out.print("直接得到域名:"+address2.getHostName());
System.out.println(" 所用时间:"+String.valueOf(System.currentTimeMillis()-time)+" 毫秒");
//通过DNS查找域名
InetAddress address3=InetAddress.getByName("210.39.3.164");
System.out.println("address3:"+address3);//域名为空
time=System.currentTimeMillis();
System.out.print("通过DNS查找域名:"+address3.getHostName());
System.out.println(" 所用时间:"+String.valueOf(System.currentTimeMillis()-time)+" 毫秒");
System.out.println("address3:"+address3);//同时输出域名和IP地址
}
}
说明使用域名创建的InetAddress对象在使用getHostName方法时并不访问DNS服务器。 而使用IP地址创建的InetAddress对象在使用getHostName方法时需要访问DNS服务器。
使用getCanonicalHostName方法获得主机名
getCanonicalHostName方法和getHostName方法功能一样,也是得到远程主机的域名。
但getCanonicalHostName得到的是主机名,而getHostName得到的主机别名。 方法定义:public String getCanonicalHostName()
该方法的返回值与InetAddress对象创建方式有关,分三种情况:
一是,若InetAddress对象是使用getLocalHost创建的,则该方法的返回值与getHostName方法得到的一样,都是本机名。
二是,若InetAddress对象是使用IP地址创建的,则该方法的返回值与getHostName方法得到的一样,它们的值可能是主机名,也可能是IP地址。
三是,若InetAddress对象是使用域名创建的,在创建InetAddress对象时,主机名和主机别名若已确定,则该方法不会访问DNS服务器,直接返回主机别名。否则,则会访问DNS服务器,则该方法的返回值取决于DNS服务器设置。
使用getCanonicalHostName方法例子
import java.net.*;
public class DomainName2 {
public static void outHostName(InetAddress address, String s)
{
System.out.println("通过"+s+"创建InetAddress对象");
System.out.println("主机名:"+address.getCanonicalHostName());
System.out.println("主机别名:" + address.getHostName());
System.out.println("");
}
public static void main(String[] args) throws Exception{
outHostName(InetAddress.getLocalHost(), "getLocalHost方法");
outHostName(InetAddress.getByName("www.ibm.com"), "www.ibm.com");
outHostName(InetAddress.getByName("www.126.com"), "www.126.com");
outHostName(InetAddress.getByName("202.108.9.77"), "202.108.9.77");
outHostName(InetAddress.getByName("211.100.26.121"),
"211.100.26.121");
}
}
使用getHostAddress方法获得IP地址
import java.net.*;
public class GetIP{
public static void main(String[] args) throws Exception{
// 输出IPv4地址
InetAddress ipv4Address1= InetAddress.getByName("1.2.3.4");
System.out.println("ipv4Address1: "+ ipv4Address1.getHostAddress());
InetAddress ipv4Address2= InetAddress.getByName("www.ibm.com");
System.out.println("ipv4Address2: "+ ipv4Address2.getHostAddress());
InetAddress ipv4Address3= InetAddress.getByName("www.szu.edu.cn");
System.out.println("ipv4Address3: "
+ ipv4Address3.getHostAddress());
// 输出IPv6地址
InetAddress ipv6Address1 = InetAddress.getByName("abcd:123::22ff");
System.out.println("ipv6Address1: "
+ ipv6Address1.getHostAddress());
InetAddress ipv6Address2 = InetAddress.getByName("www.neu6.edu.cn");
System.out.println("ipv6Address2: "
+ ipv6Address2.getHostAddress());
// 输出本机全部的IP地址
InetAddress Addresses[] = InetAddress.getAllByName("www.szu.edu.cn");
for (InetAddress address : Addresses)
System.out.println("本机地址:"+ address.getHostAddress());
}
}
使用getAddress方法获得IP地址
getAddress方法和getHostAddress类似,它们区别是getHostAddress方法返回的是字符串形式的IP地址,而getAddress方法返回的是byte数组形式的IP地址。 方法定义:public byte[] getAddress()
这个方法返回的byte数组是有符号的。在Java中byte类型的取值范围是-128〜127。 如果返回的IP地址的某个字节是大于127的整数,在byte数组中就是负数。由于Java中没有无符号byte类型,因此,要想显示正常的IP地址,必须使用int或long类型。下面代码演示了如何利用getAddress返回IP地址,以及如何将IP地址转换成正整数形式。
import java.net.*;
public class GetIP2{
public static void main(String[] args) throws Exception{
InetAddress address = InetAddress.getByName("www.csdn.net");
byte ip[] = address.getAddress();
for (byte ipSegment : ip)
System.out.print(ipSegment +" ");
System.out.println("");
for (byte ipSegment : ip)
{
int newIPSegment = (ipSegment < 0) ? 256 + ipSegment : ipSegment;
System.out.print(newIPSegment +" ");
}
}
}
//101 -55 -84 -27
//101 201 172 229
//2^7=128,8位溢出要加/减2^8=256
分析:第一行输出了未转换的IP地址,由于www.csdn.net的IP地址的第一个字节大于127,因此,输出了一个负数。而第二行由于将IP地址的每一个字节转换成了int类型,因此,输出了正常的IP地址。
获取方法
给定地址,找出主机别名:先得到InternetAddress对象,再转换得到主机别名。
import java.net.*;
public class ReverseTest{
public static void main(String[] args)throws UnknownHostException{
InetAddress ia=InetAddress.getByName("104.86.0.99");
System.out.println(ia.getCanonicalHostName());
}
}
7.测试IP地址性质编程
有时程序需要测试IP地址的性质:使用10个方法测试从命令行输入的一个地址的性质。
import java.net.*;
public class IPCharacteristics{
public static void main(String[] args){
for(String str:args){
try{
InetAddress address=InetAddress.getByName(str);
//如果是通配地址,则返回true
if(address.isAnyLocalAddress()){
System.out.println("wildcard address");
}
//如果是回送地址,则返回true
if(address.isLoopbackAddress()){
System.out.println("loopback address");
}
//如果是一个IPV6本地链接地址,则返回true
if(address.isLinkLocalAddress()){
System.out.println("link-local address");
}else if(address.isSiteLocalAddress())//是IPv6
{
System.out.println("site-local address");
}else{
System.out.println("global address");
}
//如果是一个组播地址,则返回true
if(address.isMulticastAddress()){
if(address.isMCGlobal())//全球组播地址
{
System.out.println("global multicast address");
}else if(address.isMCOrgLocal())//组织范围组播地址
{
System.out.println("organization wide multicast address");
}else if(address.isMCSiteLocal())//网站范围组播地址
{
System.out.println("site wide multicast address");
}else if(address.isMCLinkLocal())//子网范围组播地址
{
System.out.println("subnet wide multicast address");
}else if(address.isMCNodeLocal())//本地接口组播地址
{
System.out.println("interface-local multicast address");
}
}else{
System.out.println("unicast address");
}
}catch(UnknownHostException ex){
System.err.println("Could not resolve"+args[0]);
}
}
}
}
8.NetworkInterface类及编程应用
表示物理网卡或虚拟网卡:static NetworkInterface getByName(String name)
列出所有网卡:static Enumeration getInetAddresses()
import java.net.*;
import java.util.*;
public class InterfaceList {
public static void main(String[] args) throws SocketException{
try{
Enumeration<NetworkInterface> interfaces =
NetworkInterface.getNetworkInterfaces(); //显示所有接口
while(interfaces.hasMoreElements()){
NetworkInterface ni = interfaces.nextElement();
//System.out.println("‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐");
System.out.println(ni);
//System.out.println("UP: "+ ni.isUp());
}
}catch(Exception e){
System.out.println(e);
}
}
}
9.InetAddress的垃圾邮件检测编程应用(SpamCheck)
应用问题:服务器编程时,如何快速检查判断地址是否是一个已知的垃圾邮件发送者?
垃圾邮件监视服务:需求快速查询响应判断是否是一个垃圾邮件发送请求。
快速检查判断地址是否是一个已知的垃圾邮件发送者的流程:
查看一个IP地址是否是一个已知的垃圾邮件发送者:
(1)逆置此地址的字节(例如要查看IP207.34.56.23)
(2)增加黑洞服务的域( 23.56.34.207.sbl.spamhaus.org )
(3)查找此地址( 23.56.34.207.sbl.spamhaus.org 。
(4)判断:若找到此地址,说明是一个垃圾邮件发送者,否则,说明它不是。
演示运行示例程序向sbl.spamhaus.org询问某些IP地址(例如207.34.56.23 等)是否是一个垃圾邮件发送者。
public class SpamCheck{
public static final String BLACKHOLE="sbl.spamhaus.org";
public static void main(String[] args)throws UnknownHostException{
for(String arg:args){
if(isSpammer(arg)){
System.out.println(arg+" is a known spammer");
}else{
System.out.println(arg+" appears legitimate");
}
}
}
private static boolean isSpammer(String arg){
try{
InetAddress address=InetAddress.getByName(arg);
byte[] quad=address.getAddress();
String query=BLACKHOLE;
for(byte octet:quad){
int unsignedByte=octet<0?octet+256:octet;
query=unsignedByte+"."+query;//逆序
}
InetAddress.getByName(query);
return true;
}catch (UnkownHostException e){
return false;
}
}
}
10.InetAddress的离线处理日志文件编程应用
应用问题:如何通过快速离线处理日志文件来提升Web服务器的性能?
跟踪记录访问Web网站的主机,报告来访主机IP地址。
在分析日志时,需要将来访主机IP转换为主机名,例如,进行简单的日志分析处理:读取日志文件,显示各行时,将IP地址转换为主机名。如何编程处理?
日志记录项非常多,数百万行。程序的弊端?如何改进程序设计,提升性能。
使用线程池!
import java.net.*;
import java.util.concurrent.Callable;
public class LookupTask implements Callable<String>{
private String line;
public LookupTask(String line){
this.line=line;
}
@Override
public String call(){
try{
//拆分日志信息,获取IP地址
int index=line.indexOf(' ');
String address=line.substring(0,index);
String theRest=line.substring(index);
String hostname=InetAddress.getByName(address).getHostName();
}catch(Exception ex){
return line;
}
}
}
import java.io.*;
import java.util.*;
import java.util.concurrent.*;
public class PooledWeblog{
private final static int NUM_THREADS=4;
public static void main(String[] args)throws IOException{
ExecutorService executor=Executors.newFixedThreadPool(NUM_THREADS);
Queue<LogEntry> results=new LinkedList<LogEntry>();
try(BufferedReader in=new BufferedReader(
new InputStreamReader(new FileInputStream(args[0]),"UTF-8"));){
for(String entry=in.readLine();entry!=null;entry=in.readLine()){
LookupTask task=new LookupTask(entry);
Future<String> future=executor.submit(task);
LogEntry result=new LogEntry(entry,future);
results.add(result);
}
}
for(LogEntry result:results){
try{
System.out.println(result.future.get());
}catch(InterruptedException|ExecutionException ex){
System.out.println(result.original);
}
}
executor.shutdown();
}
private static class LogEntry{
String original;
Future<String> future;
LogEntry(String original,Future<String>future){
this.original=original;
this.future=future;
}
}
}
11.编程应用思考与练习
使用线程池处理服务器日志程序有何设计缺点?
若让你编程处理服务器上的日志文件,该如何设计程序?需考虑日志记录项的快速写入,快速读出分析处理,成千上万、甚至百万条日志记录文件的打开占用大量内存问题。试写一个日志处理服务程序。
试尝试使用log4j开源插件,编程实现Web服务器日志功能,主要实现日志信息的格式化定向输出存储(日志文件中)。
第七-1讲:URL和URI
1.URI与URL概念
URI:Uniform Resource Identifier
URL:Uniform Resource Locator ,用于在程序中指定网络数据源
URL标识资源,提供资源位置信息,告诉浏览器或互联网应用程序一个文档的如下信息:
完整指定的URL称为绝对URL,例如 http://www.szu.edu.cn/2014/overview.html
不完整指定的、继承了其父文档(该url所在的文档)的协议、主机名和路径的URL,称为相对URL。
在浏览http://www.szu.edu.cn/2014/overview.html时单击链接:
< a href="planning.html">校园规划< /a>
实际是访问 http://www.szu.edu.cn/2014/planning.html
相对URL优点:减少文字录入;便于网站迁移。
相对链接以/开头,表示相对于根目录而不是当前文件。
2.URL类
java.net.URL类是用java对URL的封装和定义,使用了策略(strategy)设计模式。
拓展思考:什么是策略模式,如何应用?
Final 类型,一个URL对象一旦创建后不可修改。
JVM支持的协议有http、https、jar、ftp等。
创建URL对象(6个构造方法):
URL类:协议支持(查看虚拟机支持哪些协议)
private static void testProtocol(String url){
try{
URL u=new URL(url);
System.out.println(u.getProtocol()+" is supported");
}catch(MalformedURLException ex){
String protocol=url.substring(0,url.indexOf(':'));
System.out.println(protocol +" is not supported");
}
}
//testProtocol("http://www.adc.org");
//http is supported
//testProtocol("telnet://dibner.poly.edu/");
//telnet is not supported
字符串构造URL对象
定义:public URL(String url) throws MalformedURLException
例子:
try {
URL u = new URL("http://www.szu.edu.cn/");
}catch (MalformedURLException ex) {
System.err.println(ex);
}
分块构造URL对象
通过指定协议、主机名和文件来构建一个URL,定义:public URL(String protocol, String hostname, Stringfile)throws MalformedURLException
try {
URL u = new URL("http", "www.eff.org","/blueribbon.html#intro");
//勿遗漏file参数前的/
//#隔开的intro是片段标识符
}catch (MalformedURLException ex) {
throw new RuntimeException("shouldn't happen; all VMs recognize http");
}
构造相对URL
根据相对URL和基础URL构建一个绝对URL,定义:public URL(URL base, String relative)throws MalformedURLException
从URL获取数据
1)获得InputStream:
public InputStream openStream()
2)获得URLConnection对象:
public URLConnection openConnection()
public URLConnection openConnection(Proxy proxy)
3)获得内容对象如String或Image
public Object getContent()
public Object getContent(Class[] classes)
从URL获取数据:openStream()方法
openStream方法连接到URL所引用的资源,在客户端和服务器之间建立可靠连接,返回一个InputStream,以读取数据。
下载一个Web页面,在控制台输出该页面的原始HTML。
思路:从命令行读取一个URL(http://www.oreilly.com),从这个URL打开一个InputStream,将此InputStream串链到默认编码方式的InputStreamReader,使用其read()方法从文件读取连续的字符,将字符在控制台显示输出。
import java.io.*;
import java.net.*;
import java.nio.charset.Charset;
public class SourceViewer {
public static void main(String[] args) {
if (args.length > 0) {
try {
URL u = new URL(args[0]);
URLConnection connection = u.openConnection();
// 获取内容类型和字符编码
String contentType = connection.getContentType();
if (contentType == null) {
System.err.println("Content type is not detected.");
return;
}
// 检查是否为文本类型
if (!contentType.startsWith("text")) {
System.err.println("URL does not point to a text resource.");
return;
}
// 获取字符编码
String encoding = "UTF-8"; // 默认编码
for (String param : contentType.replace(" ", "").split(";")) {
if (param.startsWith("charset=")) {
encoding = param.split("=", 2)[1];
break;
}
}
try (InputStream in = new BufferedInputStream(u.openStream());
Reader r = new InputStreamReader(in, Charset.forName(encoding))) {
int c;
while ((c = r.read()) != -1) {
System.out.print((char) c);
}
}
} catch (MalformedURLException ex) {
System.err.println(args[0] + " is not a parseable URL");
} catch (IOException ex) {
System.err.println("IOException: " + ex.getMessage());
}
} else {
System.err.println("No URL provided.");
}
}
}
编码方式的指定
通常一个页面或网络传输文件,如果使用了与ASCALL完全是不同的字符集,会在头部指明。
例 1,一个HTML文件在META标记中指明采用的是Big-5编码方式:
< meta http-equiv=“content-type” content=“text/html; charset=big5”>
例2,一个XML文件,在头部声明其编码方式:
< ?xml version="1.0" encoding="Big5”?>
从URL对象获取数据:openConnection()方法
该方法为指定的URL打开一个socket,并返回一个URLConnection对象,通过此对象可以访问协议指定的所有元数据和数据。
如果希望在程序中能与服务器直接通信,访问服务器发送的所有数据,应当使用些方法。可以访问服务器发送的原始文档(HTML/纯文本/二进制图像数据)和协议指定的所有元数据(HTTP头部,原始html)。
从URL对象获取数据:openConnection
可访问服务器发送的所有数据,如HTTP头信息。
可以向服务器发送数据,如Email,提交表单。
openConnection(Proxy proxy) 方法的重载版本,可设置代理服务器,指定通过哪个代理服务器传递连接
从URL对象获取数据:getContent()方法
该方法获取由URL引用 的数据对象,程序可尝试由它建立某种类型的数据对象。
URL u = new URL("http://mesola.obspm.fr/");
Object o =u.getContent();
//强制将对象o转换为特定类型
if(o instanceof URLImageSource){
...
该方法在从服务器获取的数据头部中查找content-type字段,如果服务器没有使用MIME头部或使用了一个陌生的content-type,该方法会返回某InputStream,供程序读取数据。
public class ContentGetter {
public static void main (String[] args) {
if (args.length > 0) {
// Open the URL for reading
try {
URL u= new URL(args[0]);
Object o= u.getContent();
System.out.println("I got a"+o.getClass().getName());
} catch (MalformedURLException ex) {
System.err.println(args[0] +" is not a parseable URL");
} catch (IOException ex) {
System.err.println(ex);
}
}
}
}
分解URL
URL类提供了9 个public方法读取URL信息
getFile(), getHost(), getPort(), getProtocol(), getRef(), getQuery(), getPath(), getUserInfo(), getAuthority()l
public String getProtocol()将url中的协议以字符串形式返回。
URL u = new URL("https://xkcd.com/a/");
System.out.println(u.getProtocol());
//显示输出字符串"https“
public String getHost()将url中的主机以字符串形式返回。
public int getPort()返回-1或url中指定的端口号。
public int getDefaultPort()返回-1或url中协议使用的默认端口。
public String getFile()把url中从第一个斜线(/)一直到片段标识符#之前的字符当作文件部分返回。
public String getPath()把url中路径和文件部分返回,不包括查询字符串。
public String getRef()返回null或url中的片段标识符部分。(#后面的)
public String getQuery()返回null或url中的查询字符串。(?后面#前面)
public String getUserInfo()把url中协议后面双斜线(//)开始一直到@之前的字符当作用户信息返回。
public String getAuthority()把url中协议后面双斜线(//)开始一直到路径之前(/之前)的字符当作授权机构信息返回。
解析URL代码例子:
import java.net.*;
public class URLSplitter {
public static void main(String args[]) {
for (int i = 0; i < args.length; i++) {
try {
URL u = new URL(args[i]);
System.out.println("The URL is " + u );
System.out.println("The scheme is " +u.getProtocol());
System.out.println("The user info is " + u.getUserInfo());
String host = u.getHost();
if (host != null) {
int atSign = host.indexOf('@');
if (atSign != -1)
host =host.substring(atSign+1);
System.out.println("The host is " + host);
} else {
System.out.println("The host is null.");
}
System.out.println("The port is " + u.getPort());
System.out.println("The path is " + u.getPath());
System.out.println("The ref is " + u.getRef());
System.out.println("The query string is " + u.getQuery());
} catch (MalformedURLException ex) {
System.err.println(args[i] + " is not a URL I understand.");
}
System.out.println();
}
}
}
比较两个URL
若想比较两个url是否一样,指向的资源是否一样,如何编程?
1)使用URL类的equals()方法。
2)或使用URL类的sameFile()方法。
这两个URL(http://www.ibiblio.org和http://ibiblio.org)一样吗?
import java.net.*;
public class URLEquality {
public static void main (String[] args) {
try {
URL www = new URL ("http://www.ibiblio.org/");
URL ibiblio = new URL("http://ibiblio.org/");
if (ibiblio.equals(www)) {
System.out.println(ibiblio + " is the same as " + www);
} else {
System.out.println(ibiblio + " is not the same as " + www);
}
} catch (MalformedURLException ex) {
System.err.println(ex);
}
}
}
//http://ibiblio.org/ is the same as http://www.ibiblio.org/
结果是一样的:因URL类的equals()方法会尝试用DNS解析url中的主机,判断
两个主机是否相同。但不会具体比较两个url标识的资源。
例如:该方法的比较结果会返回(主机名是“www.”后面的字符串)
http://www.oreilly.com与O'Reilly Media - Technology and Business Training是不等的。
http://www.oreilly.com与http://www.oreilly.com:80是不等的。
用sameFile()方法检查两个url是否指向相同资源。
例如:O'Reilly Media - Technology and Business Training 与O'Reilly Media - Technology and Business Training
用sameFile()方法检查返回结果是: 两个是相等的,即指向相同的资源。
用equals()方法返回结果是 :两个是不等的。
局限:sameFile()方法比较时不考虑片段标识符。
3.URI类
URI:Uniform Resource Identifier( Java.net.URI类)
URL:Uniform Resource Locator,例如http://www.szu.edu.cn/szu.asp
URN:Uniform Resource Name,例如urn:isbn:0-486-27557-4; mailto:feiqiao@szu.edu.cn
虽然URL可视作一种URI的抽象,但Java的URL类并不是URI的子类,URL类与URI类皆是Object的子类。
URL和URI的选用
编程实现网络数据获取功能时选用URL类。比如 想要下载一个url的内容。
编程用于解析和处理统一资源定位符相关的字符串时选用URI类。
URI类无网络获取功能。比如 想要表示一个XML命名空间。
URI类的构造方法
根据传入的字符串创建一个URI对象:
public URI(String uri) throws URISyntaxException
不检查协议,只检查传入的字符串要合uri语法规则。
URI格式:协议:协议特定部分:片段
根据传入的模式、模式特定部分、片段标识符、 字符串创建一个URI对象。
public URI(String scheme, String schemeSpecificPart, String fragment) throws URISyntaxException
Scheme: uri的协议,如http、urn、tel等,必须以字母开头,由ASCAII字符、数字和三个标点符(+ 、 - 、.)组成。为null时,省略协议,创建 一个相对uri。
Fragment: 为null时,表示忽略片段标识符。
URI各部分的获取
协议: 协议特定部分: 片段 scheme:scheme-specific-part:fragment
public String getScheme()
public String getSchemeSpecificPart()
public String getRawSchemeSpecificPart()
public String getFragment()
public String getRawFragment()
public boolean isOpaque()
4.URLEncoder类与URLDecoder类
为使各系统和平台都能正确理解和解析,URL中使用的字符必须来自ASCII的一个固定子集,即采用统一的URL字符编码。
1)大写字母A-Z
2)小写字母a-z
3)数字0-9
4)标点符号字符-_.!~*'(,)
5)标点符号字符/&?@#;$+=%
URL中的非上述字符需要转为“%字节值”,如%20:空格
URLEncoder类
Java.net.URLEncoder类用于编程准备查询字符串,从而可与使用GET方法的服务器端程序通信。
String encoded =URLEncoder.encode("Thisstringhas*asterisks", "UTF-8");
URLEncoder.encode()将字符串中的字符进行编码,使之符合URL字符规范。
使用URLEncoder编码生成查询字符串
查询串1:
https://www.google.com/search?hl=en&as_q=Java&as_epq=I/O
将查询串1编码
String query =URLEncoder.encode(https://www.google.com/search?hl=en&as_q=Java&as_epq=I/O,”UTF-8”);
System.out.println(query);
输出为:
https%3A%2F%2Fwww.google.com%2Fsearch%3Fhl%3Den%26as_q%3DJava%26as_epq%3DI%2FO
存在盲目编码问题:URLEncoder.encode()会盲目地进行编码不区分URL查询字符串中使用的特殊字符和需要编码的字符。
使用URLEncoder编码生成查询字符串
盲目编码问题的解决
String url = "https://www.google.com/search?";
url += URLEncoder.encode("hl", "UTF-8");
url += "=";
url += URLEncoder.encode("en", "UTF-8");
url += "&";
url += URLEncoder.encode("as_q", "UTF-8");
url += "=";
url += URLEncoder.encode("Java", "UTF-8");
url += "&";
url += URLEncoder.encode("as_epq", "UTF-8");
url += "=";
url += URLEncoder.encode("I/O", "UTF-8");
System.out.println(url);
//https://www.google.com/search?hl=en&as_q=Java&as_epq=I/O
URLDecoder类
public static String decode(String s, String encoding) throws UnsupportedEncodingException
解码,encode()的反变换。
对用x-www-form-url-encoded格式编码的字符串进行解码。
5.访问服务器端的GET方法
创建一个符合服务器端接收要求的查询字符串,再构造一个包含这个查询字符串的url。
1)若服务器端程序也是自己编写的,则客户端编程者也会知道服务器希望接收的名值对。
2)若服务器端使用了第三方程序,第三方程序文档也会指明它希望接收什么。
3)如果服务器端使用了第三方API, API文档会详细指用实现各种用途需发送什么数据。
GET方法适用场景
GET方法仅限于安全的操作,如:搜索请求、页面导航、设置书签、缓存、搜索、预取等。不能用于创建或修改资源等不安全操作,如:在页面上发布一条评论、订购一个比萨等。
网页表单输入的处理
互联网应用程序常需要处理表单输入。
1)编程时需先弄清楚程序希望得到什么输入。
2)创建一个查询字符串,包括必要的名值对输入。
3)构造一个包含此查询串的URL,打开URL连接,读取得到输入流。
例如:一个360购物网站的表单处理程序如何处理搜索访问表单?360购物-全网热卖商品,你爱的都在这里!
对不起,您要访问的页面暂时没有找到。当当网的搜索表单处理)
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
public class QueryString{
private StringBuilder query=new StringBuilder();
public QueryString() {
query.append("?");
}
public synchronized void add(String name, String value) {
if (query.length() > 1) {
query.append('&');
}
encode(name, value);
}
public synchronized void encode(String name,String value){
try{
query.append(URLEncoder.encode(name,"UTF-8"));
// query.append(URLEncoder.encode(name, "GBK"));
query.append('=');
query.append(URLEncoder.encode(value,"UTF-8"));
//query.append(URLEncoder.encode(value, "GBK"));
}catch(UnsupportedEncodingException ex){
throw new RuntimeException("Broken VM does not support UTF-8");
//throw new RuntimeException("Broken VM does not support GBK");
}
}
public synchronized String getQuery(){
return query.toString();
}
@Override
public String toString(){
return getQuery();
}
}
import java.io.*;
import java.net.*;
public class DMoz{
public static void main(String[] args){
String target = "";
for (int i = 0; i < args.length; i++) {
target += args[i] + " ";
}
target = target.trim();
QueryString query = new QueryString();
query.add("q", target);
try {
URL u = new URL("https://gouwu.360.cn/list" + query);
try (InputStream in = new BufferedInputStream(u.openStream());
InputStreamReader reader = new InputStreamReader(in, "GBK");
BufferedReader bufferedReader = new BufferedReader(reader)) {
String line;
while ((line = bufferedReader.readLine()) != null) {
System.out.println(line);
}
}
} catch (MalformedURLException ex) {
System.err.println("MalformedURLException: " + ex.getMessage());
} catch (IOException ex) {
System.err.println("IOException: " + ex.getMessage());
}
}
}
6.Authenticator类和PasswordAuthenticator类
Authenticator类
编程访问受口令保护的网站:基于HTTP的标准认证;Session认证; 基于Cookie的非标准认证。
Java的URL类可以访问使用HTTP认证的网站。
Java.net.Authenticator类
它为使用HTTP认证自我保护的网站提供用户名和口令:
Public abstract class Authenticator extends Object
为使URL类可以使用获得用户名和口令的子类,必须先安装认证程序:
Authenticator.setDefault(new DialogAuthenticator());
当URL类需要用用户名和口令时,使用下面的静态方法询问获得:
Public static PasswordAuthentication requestPasswordAuthentication( InetAddress address, int port, String Protocol, String prompt, String scheme) throws …
PasswordAuthenticator类
它为授权者封装和保存用户名和口令。
public final class PasswordAuthentication extends Object
PasswordAuthentivation(String userName,String password)
String getPassword()
String getUserName()
JPasswordField类
用以稍安全的方式询问用户口令的Swing组件:javax.swing.JPasswordField。
用户输入的内容会回显为*号,把口令存储为char数组,用完后可用0覆盖。Public char[ ] getPassword( )
自定义Authentication子类编程示例
自定义一个基于Swing的Authentication子类,它弹出一个对话框,向用户询问用户名和口令。其中,JPasswordField收集口令,JTextField获得用户名SecureSourceViewer类:下载由口令保护的web页面的程序
第七-2讲:HTTP
1.HTTP协议
HTTP标准
HTTP: Hypertext Transfer Protocol,超文本传输协议。
定义了web客户端如何与服务器对话,数据如何从服务器传回客户端。
可以传输 TIFF图片/Msword文档、Windows的.exe文件等。任何可以用字节表示的东西。
HTTP协议
Web浏览器<->Web服务器之间通信的标准协议:
1)客户端连接服务器80端口;
2)客户端发送请求;
3)服务器发送响应;
4)服务器关闭连接。
HTTP1.1中,第2 、 3步骤可以反复多次。因为建立连接需要较高代价,为每个请求建立一个链接代价太高。
HTTP的工作机制
无状态协议的概念
HTTP使用面向连接的TCP协议,客户端Web浏览器要与Web服务器之间建立一个TCP连接。
TCP连接建立后,浏览器进程发送HTTP请求报文,并接收应答报文。
Web服务器接收HTTP请求报文,并发送应答报文。
TCP提供可靠服务保证客户进程发送的HTTP请求正确到达服务器端。服务器进程发送HTTP应答报文也正确达到客户端。
Web服务器发送HTTP应答报文时,不保存浏览器的任何请求状态信息(属于无状态协议)
HTTP是无状态协议。
HTTP协议两种状态是非持续连接 和 持续连接
非持续连接(HTTP1.0)
每次请求/响应都要建立一次TCP连接。
例如:一个网页包括一个HTML文件和105个GIF图像文件(106个对象),那么浏览器工作过程为:缺点:必须为每个请求对象建立和维护一个新的TCP连接。
持续连接(HTTP1.1)
持续连接时,服务器在发出响应后保持该TCP连接,相同的客户端进程与服务器端之间的后续报文都通过该连接传送。
例如:一个网页包括一个HTML文件和8 个JPEG图像文件,所有请求与应答报文都通过一个持续TCP连接来传送。
工作方式:
非流水线:客户端只有在接收到前一个响应时才能发出新的请求。
流水线:客户端在没有收到前一个响应时就发出新的请求。
2.HTTP连接会话与会话保存
会话概念
用户开一个浏览器,点击多个超链接,访问服务器多个web资源,然后关闭浏览器,整个过程称之为一个会话。
有状态会话:一个同学来过教室,下次再来教室,我们会知道这个同学曾经来过,这称之为有状态会话。
会话过程中的编程问题:每个用户在使用浏览器与服务器进行会话的过程中,不可避免各自会产生一些数据,程序要想办法为每个用户保存这些会话数据。
保存会话数据的两种技术
Cookie
Cookie是客户端技术,程序把每个用户的数据以cookie的形式写给用户各自的浏览器。当用户使用浏览器再去访问服务器中的web资源时,就会带着各自的数据去。这样,web资源处理的就是用户各自的数据了。
Session
Session是服务器端技术,web服务器在运行时可以为每一个用户的浏览器创建一个其独享的session对象,由于session为用户浏览器独享,所以用户在访问服务器的web资源时,可以把各自的数据放在各自的session中,当用户再去访问服务器中的其它web资源时,其它web资源再从用户各自的session中取出数据为用户服务。
3.HTTP请求与响应
请求包括:
请求行: GET /szu.asp HTTP1.1
包含元数据的HTTP首部
格式 keyword: value
Keyword和value必须全为ASCII
行以\r\n结尾
空行
主体
一个GET请求:方法 资源路径 HTTP版本
GET /szu.asp HTTP/1.1
客户端请求
User-Agent:浏览器信息。
Host:主机名。
Accpet:text/html,application/…
--MIME类型:类型/子类型
--text/* 文本
--image/* 图像
--application/* 二进制
--application/x-* 自定义二进制
Accept: text/html, text/plain, image/gif, image/jpeg,表示客户端可处理文本类型的html文件、plain纯文本文件,图像类型的gif文件和jpeg文件。
客户端请求编程
Socket socket= new Socket("www.szu.edu.cn",80);
DataOutputStream out= new DataOutputStream( socket.getOutputStream());
BufferedReader in = new BufferedReader(new
InputStreamReader(socket.getInputStream()));
String s = "GET " + url.getPath() + " HTTP/1.1\r\n";
…
s += "\r\n";
out.writeBytes(s)
out.flush();
服务器端的HTTP响应
包含:状态行+ 信息头
状态行:HTTP/1.1 200 OK
信息头:
key:value格式描述的响应
Date
Server
Connection
Content-Type
Content-length
一个空行
Content
HTTP1.1部分响应码
2xx请求成功(200 OK)
3xx 重定位或重定向
4xx 客户端错误(400 Bad Request, 401 Unauthorized,403 Forbidden, 404 Not Found)
5xx 服务器错误(500 Internal Server Error)
请求和应答报文的交互过程
4.HTTP方法
4个方法:GET、POST、PUT、DELETE 标识可完成的操作。方法各有特定的语义,编程时必须遵循语义。
GET方法:用于获取URL所标识的一个资源的表示。
GET /register.jsp?username=elliotte&email=elharo%40ibiblio.org HTTP/1.1 Host: www.cafeaulait.org
应用场景:用于非提交的动作。如浏览一个静态Web页面。如 在购物车里增加一个商品。
POST方法:将资源的一个表示上传到已知URL的服务器,但没有指定服务器如何处理这个新提供的资源。
应用场景:用于不能重复的不安全的操作场景,常用于提交某个东西的动作。如一个交易中的下订单,此动作完成了一个提交。例子:一个POST请求,向服务器发送表单数据。
DELETE方法: 从一个指定的URL删除一个资源。
PUT方法: 将资源的一个表示上传到已知URL的服务器。
HEAD方法:仅返回资源信息头,不返回资源内容可用于检查资源是否有更新。
5.Cookie
会话跟踪编程需求: 在程序中,会话跟踪是很重要的事情。
理论上,一个用户的所有请求操作都应该属于同一个会话,而另一个用户的所有请求操作则应该属于另一个会话,二者不能混淆。例如,用户A在超市购买的任何商品都应该放在A的购物车内,不论是用户 A什么时间购买的,这都是属于同一个会话的,不能放入用户B或用户C的购物车内,这不属于同一个会话。
Web应用程序是使用HTTP协议传输数据的。HTTP协议是无状态的协议。一旦数据交换完毕,客户端与服务器端的连接就会关闭,再次交换数据需要建立新的连接。这就意味着服务器无法从连接上跟踪会话。即用户A购买了一件商品放入购物车内,当再次购买商品时服务器已经无法判断该购买行为是属于用户A的会话还是用户B的会话了。要跟踪该会话,必须引入一种机制。在Session出现之前,基本上所有的网站都采用Cookie来跟踪会话
Cookie:意为“甜饼”,是由W3C组织提出,最早由Netscape社区发展的一种机制。目前Cookie已经成为标准,所有的主流浏览器如IE、Netscape、Firefox、Opera等都支持Cookie。
Cookie技术是客户端的解决方案,Cookie就是由服务器发给客户端的特殊信息,这些信息以文本文件的方式存放在客户端,然后客户端每次向服务器发送请求的时候都会带上这些特殊的信息。
Cookie 是您访问过的网站创建的文件,用于存储浏览信息,例如您的网站偏好设置或个人资料信息。
Cookie工作原理
HTTP是一种无状态的协议,服务器单从网络连接上无从知道客户身份。
怎么办呢?( Cookie的工作原理)
就给客户端们颁发一个通行证,每人一个,无论谁访问都必须携带自己通行证。这样服务器就能从通行证上确认客户身份了。
客户端请求服务器,如果服务器需要记录该用户状态,就使用response向客户端浏览器颁发一个Cookie。客户端浏览器会把Cookie保存起来。当浏览器再请求该网站时,浏览器把请求的网址连同该Cookie一同提交给服务器。服务器检查该Cookie,以此来辨认用户状态。服务器还可以根据需要修改Cookie的内容。
Cookie实际上是一小段的文本信息。
浏览器对Cookie功能的支持
Cookie功能需要浏览器的支持。
如果浏览器不支持Cookie(如大部分手机中的浏览器)或者把Cookie禁用了,Cookie功能就会失效。
不同的浏览器采用不同的方式保存Cookie。 IE浏览器会在“C:\Documents and Settings\你的用户名\Cookies”文件夹下以文本文件形式保存,一个文本文件保存一个Cookie。
Cookie详细工作过程
1)当用户使用浏览器访问一个支持Cookie的网站的时候,用户会提供包括用户名在内的个人信息并且提交至服务器;
2)服务器在向客户端回传相应的超文本的同时也会发回这些个人信息,当然这些信息并不是存放在HTTP响应体(Response Body)中的,而是存放于HTTP响应头(Response Header);
3)当客户端浏览器接收到来自服务器的响应之后,浏览器会将这些信息存放在一个统一的位置,Windows操作系统可以从: [系统盘]:\Documents and Settings[用户名]\Cookies目录中找到存储的Cookie;
4)客户端再向服务器发送请求的时候,都会把相应的Cookie再次发回至服务器。而这次,Cookie信息则存放在HTTP请求头(Request Header)了。
有了Cookie技术实现,服务器在接收到来自客户端浏览器的请求之后,就能够通过分析存放于请求头的Cookie得到客户端特有的信息,从而动态生成与该客户端相对应的内容。通常,我们可以从很多网站的登录界面中看到 “请记住我”这样的选项,如果你勾选了它之后再登录,在下一次访问该网站的时候就不需要进行重复而繁琐的登录动作了,而这个功能就是通过Cookie实现的。
查看某个网站颁发的Cookie
打开某个网站,在浏览器地址栏输入javascript:alert (document. cookie)就可以查看该网站颁发的Cookie。(需要有网才能查看)
JavaScript脚本会弹出一个对话框显示本网站颁发的所有Cookie的内容图中弹出的对话框中显示的为Baidu网站的Cookie。其中第一行BAIDUID记录的就是运行者我的身份,只是Baidu使用特殊的方法将Cookie信息加密了。
Cookie的不可跨域名性
很多网站都会使用Cookie。例如,Google会向客户端颁发Cookie,Baidu也会向客户端颁发Cookie。
那浏览器访问Google会不会也携带上Baidu颁发的Cookie呢?或者Google能不能修改Baidu颁发的Cookie呢?
答案是否定的。Cookie具有不可跨域名性。根据Cookie规范,浏览器访问Google只会携带Google的Cookie,而不会携带Baidu的Cookie。Google也只能操作Google的Cookie,而不能操作Baidu的Cookie。
Cookie在客户端是由浏览器来管理的。
浏览器能够保证Google只会操作Google的Cookie而不会操作Baidu的Cookie,从而保证用户的隐私安全。
浏览器判断一个网站是否能操作另一个网站Cookie的依据是域名。
Google与Baidu的域名不一样,因此Google不能操作Baidu的Cookie。
例子:网站images.google.com与网站www.google.com同属于Google,但是域名不一样,二者同样不能互相操作彼此的Cookie
特殊处理:用户登录网站www.google.com之后会发现访问images.google.com时登录信息仍然有效,而普通的Cookie是做不到的。这是因为Google做了特殊处理。
Cookie编程
Java中把Cookie封装成了javax.servlet.http.Cookie类(在servlet-api.jar包中)。
每个Cookie都是该Cookie类的对象。
服务器通过操作Cookie类对象对客户端Cookie进行操作。通过request.getCookie()获取客户端提交的所有Cookie(以Cookie[]数组形式返回),通过response.addCookie(Cookie cookie)向客户端设置Cookie。
Cookie对象使用key-value属性对的形式保存用户状态,一个Cookie对象保存一个属性对,一个request或者response同时使用多个Cookie。
因为Cookie类位于包javax.servlet.http.*下面,所以JSP中不需要import该类。
Cookie编码
ASCII编码,保存英文 :只能是非空白符的ASCII文本,不包含逗号或分号。
Unicode编码,保存中文:中文与英文字符不同,中文属于Unicode字符,在内存中占4个字节,英文属于ASCII字符,内存中只占2个字节。Cookie中使用Unicode字符时需要对Unicode字符进行编码,否则会乱码。
Cookie中保存中文只能编码。一般使用UTF-8编码即可。不推荐使用GBK等中文编码,因为浏览器不一定支持,而且JavaScript也不支持GBK编码。
BASE64编码,保存二进制图片:Cookie不仅可以使用ASCII字符与Unicode字符,还可以使用二进制数据。例如在Cookie中使用数字证书,提供安全度。使用二进制数据时也需要进行编码。
注意:Cookie中可以存储二进制内容,并不实用。由于浏览器每次请求服务器都会携带Cookie,因此Cookie内容不宜过多,否则影响速度。Cookie的内容应该少而精。
Cookie作用域
set-Cookie: domain=.szu.edu.cn
受路径限制的作用域:set-Cookie: path=/home ;domain=.szu.edu.cn
其它属性设置:
set-Cookie: user=elharo; expires=Wed, 21-Dec-2015 15:23:00 GMT
set-Cookie: user=elharo; Max-Age=3600(3600秒,即1小时后过期)
CookieManager类
启用cookie: 编程时用URL类连接HTTP服务器,希望存储和获取HTTP服务器发送的cookie ,必须在程序中启用cookie。
CookieManager manager = new CookieManager();
CookieHandler.setDefault(manager);
接收策略:
CookiePolicy.ACCEPT_ALL 接受所有cookie。
CookiePolicy.ACCEPT_NONE不接受任何cookie。
CookiePolicy.ACCEPT_ORIGINAL_SERVER只接受第一方cookie。
阻塞第三方cookie,只接受第一方:
CookieManager manager = new CookieManager();
Manager.setCookiePolicy(CookiePolicy.ACCEPT_ORIGINAL_SERVER);
CookieHandler.setDefault(manager);
编程自定义CookiePolicy
通过实现CookiePolicy接口,并覆盖shouldAccept()方法:public boolean shouldAccept(URI uri, HttpCookie cookie)。
编程自定义一个简单的cookie策略类,阻塞来自.gov域的cookie,接受所有其他域的cookie。
import java.net.*;
public class NoGovernmentCookies implements CookiePolicy {
@Override
public boolean shouldAccept(URI uri, HttpCookie cookie) {
if (uri.getAuthority().toLowerCase().endsWith(".gov")|| cookie.getDomain().toLowerCase().endsWith(".gov")) {
return false;
}
return true;
}
}
CookieStore类
有时需要在应用退出时,把cookie库保存在磁盘上,下一次启动应用时再加载这些cookie。
使用CookieManager类的getCookieStore()方法,cookieManger会保存它的cookie:CookieStore store = manager.getCookieStore();
CookieStore类提供了方法用于增加、删除和列出cookie,是你能控制在正常HTTP请求和响应流之外发送的Cookie:
Public void add(URI uri, HttpCookie cookie)
Public List< HttpCookie > get(URI uri)
Public List< HttpCookie > getCookies()
Public List< URI > getURIs()
Public boolean remove(URI uri, HttpCookie cookie)
Public boolean removeAll()
Cookie store库中的每个cookie都封装在一个HTTP cookie对象中。
HttpCookie类提供了一些方法用于检查cookie的属性。
第八讲:URLConnection
1.URLConnection简介
public abstract class URLConnection extends Object
URLConnection是一个抽象类。
URLConnection -> HttpURLConnection,提供了更多与HTTP服务器的交互控制,可以检查服务器发送的首部,做出响应。可以设置客户端请求中使用的首部字段。可用GET、POST、PUT等HTTP请求方法向服务器发送数据。
URLConnection类的使用步骤
直接使用URLConnection类的程序基本编写步骤:
1)创建URL对象
2)调用URL.openConnection()获取URLConnection对象
3)配置URLConnection对象
4)读取头信息
5)获得输入流,并读数据
6)获得输出流,并写数据
7)关闭连接
创建URLConnection对象
URLConnection类仅有一个为保持类型的构造函数:protected URLConnection(URL url)。
例如要编写一个协议处理器需派生URLConnection的子类才会使用此构造函数。通常使用该类的openConnection方法来创建对象。
try {
URL u = new URL("http://www.szu.edu.cn/");
URLConnection uc =u.openConnection();
// read from the URL...
}catch (MalformedURLException ex) {
System.err.println(ex);
}catch (IOException ex) {
System.err.println(ex);
}
2.读取服务器数据
使用一个URLConnection对象从一个URL获取数据,编程基本步骤:
1)构造一个URL对象
2)调用这个URL对象的openConnection()方法获取此URL的URLConnection对象。
3)调用这个URLConnection的getInputStream()方法
4)使用通常的流API读取输入流
URL connection的getInputStream( )方法返回一个通用Input Stream,可以读取和解析服务器发送的数据。
例子:使用URLConnection getInputString( )方法下载一个web页面。
import java.io.*;
import java.net.*;
public class SourceView2{
public static void main(String[] args){
if(args.length>0){
try{
URL u=new URL(args[0]);
URLConnection uc=u.openConnection();
try(InputStream raw=uc.getInputStream()){
InputStream buffer=new BufferedInputStream(raw);
Reader reader=new InputStreamReader(buffer);
int c;
while((c=reader.read())!=-1){
System.out.print((char)c);
}
}catch(MalformedURLException ex){
System.err.println(args[0]+" is not a parseable URL");
}catch(IOException ex){
System.err.println(ex);
}
}
}
}
}
URL与URLConnection区别
URLConnection提供了对HTTP头信息访问。
URLConnection可以配置发给服务器的请求参数。
URLConnection除了可读取服务器数据外,还可向服务器写入数据。
读取HTTP首部
一个HTTP服务器响应的头部信息头部中的常用 字
段有:
Content-type
Content-length
Content-encoding
Date
Last-modified
Expires
一个Appache Web服务器返回的响应信息中的HTTP头部:
HTTP/1.1 301 Moved Permanently
Date: Sun, 21 Apr 2013 15:12:46 GMT
Server: Apache
Location: The Public's Library and Digital Archive
Content-Length: 296
Connection: close
Content-type: text/html; charset = iso-8859-1
头字段信息获取的方法(2个)
public Steing getContentType()
返回响应主体的MIME类型,包括字符编码方式,如:text/html; charset=UTF-8
使得编程人员可以使用HTTP头部中指定的编码方式对文档解码。如果头部中没有指定编码方式,就使用ISO-8859-1,这是HTTP 的默认编码方式。
public int getContentLength()
public long getContentLengthLong()
返回响应主体的字节数。
如无Content-length首部,返回-1。
当程序中需要准备知道要读取的字节数,或需要预先创建一个足够大的缓冲区来保存数据时使用。
例子:用正确的字符集下载一个Web页面。
import java.io.*;
import java.net.*;
public class EncodingAwareSourceViewer{
public static void main(String[] args){
for(int i=0;i<args.length;i++){
try{
String encoding = "ISO-8859-1";//默认编码方式
URL u=new URL(args[i]);
URLConnection uc=u.openConnection();
String contentType=uc.getContentType();
int encodingStart=contentType.indexOf("charset=");
if(encodingStart!=-1){
encoding=contentType.substring(encodingStart+8);//设置正确的编码,+8是因为“charset=”
}
InputStream in=new BufferedInputStream(uc.getInputStream());
Reader r=new InputStreamReader(in,encoding);//指定字符读入形式(Reader)
int c;
while((c=r.read())!=-1){
System.out.print((char)c);
}
r.close();
}catch(MalformedURLException ex){
System.err.println(args[0]+" is not a parseable URL");
}catch(UnsupportedEncodingException ex){
System.err.println("Server sent an encoding Java does not support:"+ex.getMessage());
}catch(IOException ex){
System.err.println(ex);
}
}
}
}
例子:从Web网站下载二进制文件并保存到磁盘
import java.io.*;
import java.net.*;
public class BinarySaver{
public static void main(String[] args){
for(int i=0;i<args.length;i++){
try{
URL root=new URL(args[i]);
saveBinaryFile(root);
}catch(MalformedURLException ex){
...
}catch(IOException ex){
...
}
}
}
public static void saveBinaryFile(URL u)throws IOException{
URLConnection uc=u.openConnection();
String contentType=uc.getContentType();
int contentLength=uc.getContentLength();
if(contentType.startsWith("text/")||contentLength==-1){
throw new IOException("This is not a binary file");
}
try(InputStream raw=uc.getInputStream()){
InputStream in=new BufferedInputStream(raw);
byte[] data=new byte[contentLength];
int offset=0;
while(offset<contentLength){
int bytesRead=in.read(data,offset,data.length-offset);
if(bytesRead==-1)break;
offset+=bytesRead;
}
if(offset!=contentLength){
throw new IOException("Only read "+offset+" bytes;Expected "+contentLength+" bytes");
}
String filename=u.getFile();
filename=filename.substring(filename.lastIndexOf('/')+1);//最后一个子文件
try(FileOutputStream fout=new FileOutputStream(filename)){
fout.write(data);//写入
fout.flush();
}
}
}
}
头字段信息获取的方法(4个)
public String getContentEncoding():返回内容压缩编码(与字符编码不同)
public long getDate():发送文档的时间
public long getExpiration():文档失效时间
public long getLastModified():文档最后修改时间
例子:从命令行读取url,使用访问头部信息的6个方法,显示内容类型、内容长度、内容编码方式、最后修改日期、过期日期和当前日期。
import java.io.*;
import java.net.*;
import java.util.*;
public class HeaderViewer{
public static void main(String[] args){
for(int i=0;i<args.length;i++){
try{
URL u=new URL(args[i]);
URLConnection uc=u.openConnection();
System.out.println(uc.getContentType());
if(uc.getContentEncoding()!=null){
System.out.println(uc.getContentEncoding());
}
if(uc.getDate()!=0){
System.out.println(new Date(uc.getDate());
}
if(uc.getLastModified()!=0){
System.out.println(new Date(uc.getLastModified());
}
if(uc.getExpiration()!=0){
System.out.println(new Date(uc.getExpiration());
}
if(uc.getContentLength()!=-1){
System.out.println(uc.getContentLength());
}
}catch(MalformedURLException ex){
...
}catch(IOException ex){
...
}
System.out.println();
}
}
}
获取任意头字段方法
public String getHeaderField(String name)
String contentType = uc.getHeaderField("content-type");
String contentEncoding = uc.getHeaderField("content-encoding"));
String data = uc.getHeaderField("date");
String expires = uc.getHeaderField("expires");
String contentLength = uc.getHeaderField("Content-length");
使用获取任意首部字段方法显示整个HTTP请求
编程实现:提供任一个url,在控制台显示输出HTTP服务器响应整个头部。
获得任意头字段的方法:
public String getHeaderFieldKey(int n):第 n个字段名
public String getHeaderField(int n):第 n个字段值
这两个方法可以实现对所有头字段的遍历(因为无法预知HTTP服务器会发送哪些字段)
import java.io.*;
import java.net.*;
public class AllHeaders{
public static void main(String[] args){
for(int i=0;i<args.length;i++){
try{
URL u=new URL(args[i]);
URLConnection uc=u.openConnection();
for(int j=1;;j++){
String header=uc.getHeaderField(j);
if(header==null)break;
System.out.println(uc.getHeaderwFieldKey(j)+": "+header);
}
}catch(MalformedURLException ex){
...
}catch(IOException ex){
...
}
System.out.println();
}
}
}
3.缓存
为啥需要缓存?
比如Web浏览器从远程HTTP服务器请求加载一次一个页面和图片后,保存在本地缓存中,每次需要时从缓存加载,不是每次都从远程服务器加载。
默认:使用GET通过HTTP访问的页面可以缓存,也应缓存。使用HTTPS或POST访问的页面不应缓存。
通过检查和使用HTTP响应头部(Expires和Cache-Contorl)可以控制缓存。
Expires:指示可以缓存这个资源的表示,直到指定的时间为止。
Cache-Control:提供了细粒度的缓冲策略。(课后熟悉P198页各属性的含义)
HTTP客户端检查Cache-control头部,可以利用信息:
1)若本地缓存有此资源的一个表示,且未过期,可直接使用此资源,无需与服务器交互。
3)若本地缓存有此资源的一个表示,且已过期,在完成完整 GET之前,可检查服务器的HEAD首部,查看资源是否已改变。
Java的Web缓存
JVM只支持一个共享缓存,在HTTP客户端程序中要使用缓存需要使用到自定义的3个具体子类来安装URL类使用的系统级缓存。
ResponseCache的一个具体子类
CacheRequest的一个具体子类
CacheResponse的一个具体子类
使用ResponseCache.setDefault()方法把自定义缓存子类对象安装为默认缓存,来处理自定义的CacheRequest和CacheResponse子类。
CacheRequest类
ResponseCache的put()方法返回一个CacheRequest类型的对象。
CacheRequest类表示在 ResponseCache 中存储资源的通道。
CacheRequest类的实例包装了一个OutputStream对象,HTTP客户端程序可以调用该对象来将资源数据存储到缓存中。
Package java.net
Public abstract class CacheRequest {
//返回可以将响应正文写入cache库的 OutputStream
public abstract OutputStream getBody( ) throws IOException;
public abstract void abort( );//中止缓存响应的尝试
}
自定义CacheRequest子类
自定义一个CacheRequest子类,可返回一个ByteArrayOutputStream,支持ResponseCache对象的put()方法将资源数据以字节数组文件存入缓存中。
import java.io.*;
import java.net.*;
public class SimpleCacheRequest extends CacheRequest{
private ByteArrayOutputStream out=new ByteArrayOutputStream();
@override
public OutputStream getBody() throws IOException{
return out;
}
@Override
public void abort(){
out.reset();
}
public byte[] getData(){
if(out.size()==0)return null;
else return out.toByteArray();
}
}
CacheResponse类
ResponseCache的get()方法从缓存中获取资源的数据和首部,包装在一个CacheRsponse类型的对象中返回。
CacheRsponse类表示在 ResponseCache 中检索资源的通道。
CacheRsponse类的实例包装了一个URI请求资源的数据和首部,HTTP客户端程序可以调用该对象将资源的数据或首部从缓存中取出。
Package java.net
Public abstract class CacheResponse{
//返回响应的主体作为InputStream
public abstract Map<String, List<String>> getHeaders( ) throws IOException;
public abstract InputStream getBody( ) throws IOException;//返回响应头作为一个Map
}
自定义CacheResponse子类
自定义一个CacheResponse子类,与一个自定义的CacheRequest子类(SimpleCacheRequest)对象和一个CacheControl对象绑定,支持ResponseCache对象的get()方法从缓存中读取资源的数据或首部。
import java.io.*;
import java.net.*;
import java.util.*;
public class SimpleCacheResponse extends CacheResponse{
private final Map<String,List<String>>headers;
private final SimpleCacheRequest request;
private final CacheControl control;
public SimpleCacheResponse(
SimpleCacheRequest request,URLConnection uc,CacheControl)
throws IOException{
this.request=request;
this.control=control;
this.expires=new Date(uc.getExpiration());
this.headers=Collection.unmodifiableMap(uc.getHeaderFields());
}
@Override
public InputStream getBody(){
return new ByteArrayInputStream(request.getData());
}
@Override
public Map<String,List<String>> getHeaders()
throw IOException{
return headers;
}
public CacheControl getControl{
return control;
}
public boolean isExpired(){
Date now=new Date();
if(control.getMaxAge().before(now))return true;
else if(expires!=null&&control.getMaxAge()!=null){
return expires.before(now);
}else{
return false;
}
}
}
ResponseCache类
代表URLConnection缓存的实现。
它的实例可以通过ResponseCache.setDefault(ResponseCache)向系统注
册,系统将调用这个对象:
1)存储从外部源已检索资源数据到缓存中。
2)试图获取可能是存储在缓存中的所请求的资源。
ResponseCache实现决定哪些资源应该被缓存,多长时间。如果不能从缓存检索请求的资源, 协议处理程序将从其原始位置获取资源。
自定义ResponseCache子类
自定义一个ResponseCache子类,使用一个大的线程安全HashMap将有限数量的响应存储在内存中。
import java.io.*;
import java.net.*;
import java.util.*;
import java.util.concurrent.*;
public class MemoryCache extends ResponseCache{
private final Map<URI,SimpleCacheResponse> responses=new ConcurrentHashMap<URI,SimpleCache<Response>();
public MemoryCache(){
this(100);
}
public MemoryCache(int maxEntries){
this.maxEntries=maxEntries;
}
@Override
public CacheRequest put(URI uri,URLConnection conn)throw IOException{
if(responses.size()>=maxEntries)return null;
CacheControl control=new CacheControl(conn.getHeaderField("Cache-Control"));
if(control.noStore()){
return null;
}else if(!conn.getHeaderField(0),startsWith("GET ")){
//只能是GET方法
return null;
}
SimpleCacheRequest request=new SimpleCacheRequest();
SimpleCacheResponse response=new SimpleCacheResponse(request,conn,control);
responses.put(uri,response);
//ResponseCache的put()方法返回一个CacheReques类型的对象。
return request;
}
@Override
public CacheResponse get(URI uri,String requestMethod,Map<String,List<String>> requestHeaders)throw IOException{
if("GET".equals(requestMethod)){
SimpleCacheResponse response=responses.get(uri);
//判断过期日期
if(response!=null&&response.isExpired()){
//过期了
responses.remove(response);
response=null;
}
return response;
}else{
return null;
}
}
}
4.连接与请求配置
配置URLConnection连接
抽象类 URLConnection 是一个超类,代表应用程序和 URL 之间的通信链接。
此类的实例可用于读取和写入此 URL 引用的资源。创建一个到URL 的连接需要4个步骤:
1)通过在 URL 上调用 openConnection 方法创建连接对象。
2)处理设置参数和一般请求属性。
3)使用 connect 方法建立到远程对象的实际连接。
4)远程对象变为可用。远程对象的头字段和内容变为可访问。
URLConnection类有7个受保护的实例字段,通过对这些字段的访问和修改可以配置和定义客户端如何向服务器做出请求。这些字段的访问和修改需使用字段对应的get和set方法。
doOutput=true;//表示除了通过这个URLConnection读取数据外,还可以将数据写入到服务器。
useCaches=false;//表示连接会绕过所有本地缓存,重新从服务器下载文件。
Protected URL url 字段
url字段指定了这个URLConnection连接的URL。构造函数会在创建URLConnection时设置这个字段,此后不能再改变。可调用getURL()获取该字段的值
Protected boolean connected
当连接已经打开,connected为true。由任何导致URLConnection连接的方法设置。当连接关闭,connected 为false。 没有直接读取或改变connected值的方法该变量只能由java.net.URLConnection及其子类的实例访问。注意:派生URLConnection子类编写一个协议处理器时,要正确设置该字段的值,否则程序可能出现严重的且很难诊断的bug。
Protected boolean allowUserInteraction
该字段指示是否允许用户交互,默认值是false。当连接关闭,connected 为false。只能通过该字段对应的get/set方法访问。只能在URLConnection连接前设置(在建立输入连接之前),否则会抛出IllegalStateException异常。
Protected boolean doInput
该字段指示URLConnection可否用于写入服务器。doInput为true表示连接写用于读/写服务器。doInput为false表示连接只能用于读取服务器。 默认值是true。该字段的访问使用对应的get/set方法。
Protected boolean doOutput
该字段指示程序可否使用URLConnection将输出发回服务器。doOutput为true表示连接写用于写数据到服务器。doOutput为false表示连接不能用于写数据到服务器。 默认值是false。 该字段的访问使用对应的get/set方法。
Protected boolean ifModifiedSince
该字段指示放置在URLConnection连接请求头的IfModified-Since字段中的日期。该字段值是long型, 是自格林尼治标准时间1970.1.1 00:00:00后的毫秒数。
访问方法:
Public long getIfModifiedSince()
Public void setIfModifiedSince(long ifModifiedSince)
服务器考虑IfModified-Since字段,如果请求的文档在这个时间后有修改,服务器就发送此文档,否则不发送此文档,回复“未修改”消息。客户端从本地缓存中加载此文档。
ifModifiedSince应用例子:打印ifModifiedSince的默认值,将此值设置为24小时之前,并打印这个新值。 然后下载并显示文档,但只是在最后24小时内有所修改时才会显示文档。
import java.io.*;
import java.net.*;
import java.util.*;
public class Last24 {
public static void main (String[] args) {
// Initialize a Date object with the current date and time
Date today = new Date();
long millisecondsPerDay = 24 * 60 * 60 * 1000;
for (int i = 0; i < args.length; i++) {
try {
URL u = new URL(args[i]);
URLConnection uc = u.openConnection();
System.out.println("Original if modified since: "
+ new Date(uc.getIfModifiedSince()));
uc.setIfModifiedSince((new Date(today.getTime()
- millisecondsPerDay)).getTime());
System.out.println("Will retrieve file if it's modified since "
+ new Date(uc.getIfModifiedSince()));
try (InputStream in = new BufferedInputStream(uc.getInputStream())) {
Reader r = new InputStreamReader(in);
int c;
while ((c = r.read()) != -1) {
System.out.print((char) c);
}
System.out.println();
}
} catch (IOException ex) {
System.err.println(ex);
}
}
}
}
Protected boolean useCaches
有些客户端(比如web浏览器)可以从本地缓存获取文档applet可以访问浏览器的缓存。 独立的应用程序可使用java.net.ResponseCache类。如果有缓存,那么由useCaches变量确定是否可以使用缓存useCahces变量为true表示将使用缓存,false表示不使用缓存,默认值是true。
编程时,使用该变量对应的get和set方法访问此变量在程序中禁用缓存,确保总是获取文档的最新版本。通过将useCaches设置为false来实现。
配置HTTP客户端请求头信息
HTTP客户端向服务器发送一个请求行和一个首部。Web服务器根据这些信息向不同的客户端 提供不同的页面。
通过设置客户端请求头和服务器端响应头中的字段来控制请求格式和响应格式。
需在打开连接前设置字段
仅对HTTP协议适用,设置方法:
public void setRequestProperty(String name, String value)
public void addRequestProperty(String name, String value)
Cookie是名-值对的集合。HTTP客户端和服务器之间使用cookie存储一些受限的持久信息。服务器使用HTTP响应头部向客户端发送一个cookie,之后,客户端请求此服务器的URL,都会在HTTP请求头部中包含此cookie字段:
Cookie: usename=elharo; password=ACD0X9F23JJJn6G; session=100678945
给定一个URLConnection对象uc,如何将这个cookie增加到连接?
uc.setRequestProperty("Cookie",
"username=elharo; password=ACD0X9F23JJJn6G; session=100678945")
5.向服务器写入数据
当在客户端程序中需要向URLConnection写入数据,例如使用POST向Web服务器提交表单,或使用PUT上传文件。则使用URLConnection的getOutputStream()方法返回一个OutputStream,用来写入数据传送给服务器。得到OutputStream后,将它串链到缓冲类等方便写的类,如缓冲类BufferedOutputStream或BufferedWriter,写入类DataOutputStream或OutputStreamWriter。
try{
URL u = new URL("http://www.somehost.com/cgi-bin/acgi");//打开连接,准备post
URLConnection uc = u.openConnection();
uc.setDoOutput(true);
OutputStream raw = uc.getOutputStream();
OutputStream buffered = new BufferedOutputStream(raw);
OutputStreamWriter out = new OutputStreamWriter(buffedred, “8859-1”);
out.write(“first=Julie&middle=&last=Harting&work=String+Quartet\r\n”);
out.flush();
out.close();
}catch(IOExceptionex){
System.err.println(ex);
}
POST请求
用POST发送请求与GET的区别是:
1)先调用setDoOutput(true)。
2)使用URLConnection的getOutputStream()方法写入查询字符串,而不是附加到URL(发送x-www-form-url-encoded码的字符串)。
Java会缓冲写入输出流的所有数据,直到流关闭为止。
例子:编写程序,向一个HTTP服务器提交一个表单:使用URLConnection类和第5章的QueryString类提交(post)表单数据。向服务器资源发送名字”Elliotte Rusty Harold”和电子邮件地址elharo@biblio.org。服务器资源位于http://www.cafeaulait.org/books/jnp4/postquery.html, 是一个表单测试器,可接受任何使用POST或GET方法的输入,并返回一个HTML页面,显示所提交的名和值。返回的数据是HTML。
import java.io.*;
import java.net.*;
public class FormPoster {
private URL url;
// from Chapter 5, Example 5-8
private QueryString query = new QueryString();
public FormPoster (URL url) {
if (!url.getProtocol().toLowerCase().startsWith("http")) {
throw new IllegalArgumentException(
"Posting only works for http URLs");
}
this.url = url;
}
public void add(String name, String value) {
query.add(name, value);
}
public URL getURL() {
return this.url;
}
public InputStream post() throws IOException {
// open the connection and prepare it to POST
URLConnection uc = url.openConnection();
uc.setDoOutput(true);
try (OutputStreamWriter out
= new OutputStreamWriter(uc.getOutputStream(), "UTF-8")) {
// The POST line, the Content-type header,
// and the Content-length headers are sent by the URLConnection.
// We just need to send the data
out.write(query.toString());
out.write("\r\n");
out.flush();//不能漏掉此句,否则不会发送任何数据
}
// Return the response(读入的)
return uc.getInputStream();
}
public static void main(String[] args) {
URL url;
if (args.length > 0) {
try {
url = new URL(args[0]);
} catch (MalformedURLException ex) {
System.err.println("Usage: java FormPoster url");
return;
}
} else {
try {
url = new URL(
"http://www.cafeaulait.org/books/jnp4/postquery.phtml");
} catch (MalformedURLException ex) { // shouldn't happen
System.err.println(ex);
return;
}
}
FormPoster poster = new FormPoster(url);
poster.add("name", "Elliotte Rusty Harold");
poster.add("email", "elharo@ibiblio.org");
try (InputStream in = poster.post()) {
// Read the response
Reader r = new InputStreamReader(in);
int c;
while((c = r.read()) != -1) {
System.out.print((char) c);
}
System.out.println();
} catch (IOException ex) {
System.err.println(ex);
}
}
}
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
public class QueryString {
private StringBuilder query = new StringBuilder();
public QueryString() {
}
public synchronized void add(String name, String value) {
query.append('&');
encode(name, value);
}
private synchronized void encode(String name, String value) {
try {
query.append(URLEncoder.encode(name, "UTF-8"));
query.append('=');
query.append(URLEncoder.encode(value, "UTF-8"));
} catch (UnsupportedEncodingException ex) {
throw new RuntimeException("Broken VM does not support UTF-8");
}
}
public synchronized String getQuery() {
return query.toString();
}
@Override
public String toString() {
return getQuery();
}
}
POST请求例子分析与测试
FormPoster使用URLConnection类和QueryString类提交POST表单数据。
在构造函数中设置URL。查询字符串用add()方法构建。 Post()方法负责向服务器发送数据,返回包含服务器响应的输入流。
Main()方法实现测试,可向位于Query Results的资源,发送名字“Elliotte Rusty Harold”和电邮地址elharo@ibiblio.org。此Web资源是一个表单测试器,接受任何使用POST或GET方法的输入,并返回一个未解析的HTML页面,显示提交的名和值。
提交表单数据编程总结
1)确定要发送给服务器的程序的名-值对。
2)编写接受和处理请求的服务器端程序。如果没有使用定制数据编码,可以使用普通的HTML表单和Web浏览器测试此程序。
3)在java程序中创建一个查询字符串。字符串形式:name1=value1&name2=value2&name3=value3 在增加到查询字符串之前,先将各个名和值传递到URLEncoder.encode()。
4)打开一个URLConnection,指向将接受数据的程序的URL。
5)调用setDoOutput(true)设置doOutput为true。
6)将查询字符串写入到URLConnection的OutputStream。
7)关闭URLConnection的OutputStream。
8)从URLConnection的InputStream读取服务器响应。
猜测MIME媒体类型
异构的网络环境,并非所有协议和服务器都会使用标准MIME类型正确地指定所传输的文件的类型。
FTP协议比MIME标准出现的早,这些老协议如何处理?
有些使用了MIME的HTTP服务器不提供MIME首部,或者提供了不正确的首部。我们在编程时需要确定数据的MIME类型。有两个方法可调用
public static String guessContentTypeFromName(String name):根据文件名猜测类型
public static String guessContentTypeFromStream(InputStream in):根据流中前几字节数据猜测内容类型(in必须支持标记,读取了前面的字节之后,可以返回流开始处)
优先采用第一个方法。
6.HttpURLConnection
Java.net.HttpURLConnection类是URLConnection类的抽象子类。包含更多处理HTTP请求方法和HTTP响应码常量,如获得和设置请求方法、确定是否重定向、获得响应码和消息、确定是否使用了代理服务器。 是抽象类,不能直接创建其对象,需使用强制转换方式:
public void setRequestMethod(String method) throws ProtocolException,设置请求方法:GET、POST、HEAD、PUT、DELETE、OPTIONS、TRACE 需要全大写。
HttpURLConnection与URLConnection
URLConnection与HttpURLConnection都是抽象类,无法直接实例化对象。其对象主要通过URL的openconnection方法获得。
openConnection方法只创建URLConnection或者HttpURLConnection实例,并不进行真正的连接操作。并且,每次openConnection都将创建一个新的实例。
openConnection不进行的连接操作的原因在于,可以在连接操作进行之前,对URLConnection或者HttpURLConnection实例的某些属性进行设置,如设置超时值等。
无论URLConnection或者HttPURLConnection实例,其getInputStream之类属于应用层的操作,都会调用connect操作。但是,connectTimeout与ReaderTimeout并不相同。有可能在已连接的情况下,仍然Reader超时。
请求方法
Web客户端与Web服务器联系时,发送的第一个内容是请
求行。如:
GET catalog/jfcnut/index.html HTTP/1.0
GET方法:Web客户端从服务器获取文件。
POST方法:客户端向表单提交数据发送请求并获得响应。
PUT方法:将文件上传到服务器。
DELETE方法:删除服务器的文件。
HEAD方法:只请求文档的首部。
OPTIONS方法:向服务器询问一个指定URL支持的选项列表。
TRACE方法:跟踪请求本身。
public void setRequestMethod(String method) throws ProtocolException
设置请求方法:GET、POST、HEAD、PUT、DELETE、OPTIONS、TRACE 需要全大写。
HEAD方法
请求信息跟GET同,告诉服务器只返回HTTP头部,不用实际发送文件。服务器只返回头信息。此方法常用于检查文件在最后一次缓存之后是否有修改。减少不必要信息的获取和传输,使程序高效。编程使用HEAD请求方法,获得和显示URL文件在服务器上的最后一次修改时间。
import java.io.*;
import java.net.*;
import java.util.*;
public class LastModified {
public static void main(String[] args) {
for (int i = 0; i < args.length; i++) {
try {
URL u = new URL(args[i]);
HttpURLConnection http = (HttpURLConnection) u.openConnection();
http.setRequestMethod("HEAD");
System.out.println(u + " was last modified at "
+ new Date(http.getLastModified()));
} catch (MalformedURLException ex) {
System.err.println(args[i] + " is not a URL I understand");
} catch (IOException ex) {
System.err.println(ex);
}
System.out.println();
}
}
}
DELETE方法
DELETE方法用于删除web服务器上位于指定URL的文件。该方法存在安全风险,不少服务器的配置可能不支持此方法。通常要求完成身份认证才支持.。
服务器可以拒绝此请求,或要求提供身份认证。若接受请求,如何响应删除取决于服务器厂商的实现细节,可能会删除文件、或将文件移到回收站中、或将文件标记为不可读。
HTTP/1.1 405 Method Not Allowed
PUT方法
此方法允许客户端将文档放在网站的抽象层次结构中,但不需要知道网站如何映射到实际的本地文件系统。PUT方法的使用通常需要身份认证,且服务器必须特别配置支持PUT方法,多数服务器不直接支持PUT方法。一个编辑器如何用PUT将一个文件存放在Web服务。
OPTIONS方法
OPTIONS请求方法询问某个特定的URL支持哪些选项。如果请求的URL是星号(*),此请求将应用于整个服务器而不是服务器上的某个URL. 例如请求:
OPTIONS /xml/ HTTP/1.1
Host: www.ibiblio.org
Accept: text/html, image/gif, image/jpeg, *; q=.2, /; q=.2
Connection: close
服务器对OPTIONS请求的响应:
HTTP/1.1 200 OK
Date: Sat, 04 May 2013 13:52:53 GMT
Server: Apache
Allow: GET,HEAD,POST,OPTIONS,TRACE
Content-Style-Type: text/css
Content-Length: 0
Connection: close
Content-Type: text/html; charset=utf-8
TRACE方法
TRACE方法会发送HTTP头部,服务器从客户端接收此HTTP头部,用于查看服务器和客户端之间的代理服务器做了哪些修改。
断开与服务器的连接
HTTP1.1支持持久连接,使用Keep-Alive时,服务器不会因为已经向客户端发送了最后一个字节的数据就立即关闭连接。最好由客户端在确认工作结束时关闭连接。在服务器关闭连接之前,如果再次连接同一个服务器,HttpURLConnection类会重用socket。客户端需调用方法显式断开连接:
public void HttpURLConnection.disconnect()
若连接上有打开的流:断开连接可以关闭流;关闭流不会断开连接。
处理服务器的响应
200 OK响应, 表示所请求的文档已找到。
301 响应码表示所请求的资源已经永久移动到一个新位置,浏览器应当重定向到这个新位置,并更新所有指向老位置的书签。
404 NotFound响应,表示所请求的URL不再指向一个文档。
获得响应码:
public int getResponseCode() throws IOException
响应码后面的文本字符串称为响应消息(response message)
public String getResponseMessage() throws IOException:获取响应消息
默认情况下,HttpURLConnection会跟随重定向,遇到300级响应码时,浏览器自动从新位置加载文档。为防止安全风险,可以调用方法确定是否跟随重定向:
public static boolean getFollowRedirects():返回true表示跟随重定向,否则返回false
public static void setFollowRedirects(boolean follow)设置为true表示允许HttpURLConnection对象跟随重定向,设置为false表示阻止跟随。
HttpURLConnection类包括36个命名常量,表示常见的响应码,例如 HttpURLConnection.OK,例如 HttpURLConnection.NOT_FOUND
例子:修改了之前的源码查看程序,可以查看响应消息。
import java.io.*;
import java.net.*;
public class SourceViewer3 {
public static void main (String[] args) {
for (int i = 0; i < args.length; i++) {
try {
// Open the URLConnection for reading
URL u = new URL(args[i]);
HttpURLConnection uc = (HttpURLConnection) u.openConnection();
int code = uc.getResponseCode();
String response = uc.getResponseMessage();
System.out.println("HTTP/1.x " + code + " " + response);
for (int j = 1; ; j++) {
String header = uc.getHeaderField(j);
String key = uc.getHeaderFieldKey(j);
if (header == null || key == null) break;
System.out.println(uc.getHeaderFieldKey(j) + ": " + header);
}
System.out.println();
try (InputStream in = new BufferedInputStream(uc.getInputStream())) {
// chain the InputStream to a Reader
Reader r = new InputStreamReader(in);
int c;
while ((c = r.read()) != -1) {
System.out.print((char) c);
}
}
} catch (MalformedURLException ex) {
System.err.println(args[0] + " is not a parseable URL");
} catch (IOException ex) {
System.err.println(ex);
}
}
}
}
错误流的读取
服务器遇到一个错误,仍会在消息主体中返回有用的信息,例如客户端向网站请求的页面http://www.cafeaulait.org/sliders不存在时,服务器返回404错误码的同时,会发送一个搜索页面,帮助用户确定缺少的页面可能在哪里。
Public InputStream getErrorStream():getInputStream失败后,可在catch块中调用此方法获得错误流。
import java.io.*;
import java.net.*;
public class SourceViewer4 {
public static void main (String[] args) {
try {
URL u = new URL(args[0]);
HttpURLConnection uc = (HttpURLConnection) u.openConnection();
try (InputStream raw = uc.getInputStream()) {
printFromStream(raw);
} catch (IOException ex) {
printFromStream(uc.getErrorStream());
}
} catch (MalformedURLException ex) {
System.err.println(args[0] + " is not a parseable URL");
} catch (IOException ex) {
System.err.println(ex);
}
}
private static void printFromStream(InputStream raw) throws IOException {
try (InputStream buffer = new BufferedInputStream(raw)) {
Reader reader = new InputStreamReader(buffer);
int c;
while ((c = reader.read()) != -1) {
System.out.print((char) c);
}
}
}
}
利用HttpURLConnection对象和Internet交互
从Internet获取网页
发送请求,将网页以流的形式读回来.
1)创建一个URL对象:URL url = new URL("http://www.sohu.com");
2)利用HttpURLConnection对象从网络中获取网页数据: HttpURLConnection conn = (HttpURLConnection) url.openConnection();
3)设置连接超时:conn.setConnectTimeout(6* 1000);
4)对响应码进行判断:if (conn.getResponseCode() != 200) throw new RuntimeException("请求url失败");
5)得到网络返回的输入流:InputStream is = conn.getInputStream();
6)String result = readData(is, "GBK");conn.disconnect();
从Internet获取文件
利用HttpURLConnection对象,我们可以从网络中获取文件数据
1)创建URL对象,并将文件路径传入:URL url = new URL("http://photocdn.sohu.com/20100125/Img269812337.jpg");
2)创建HttpURLConnection对象,从网络中获取文件数据:HttpURLConnection conn = (HttpURLConnection) url.openConnection();
3)设置连接超时:conn.setConnectTimeout(6* 1000);
4)对响应码进行判断:if (conn.getResponseCode() != 200) throw new RuntimeException("请求url失败");
5)得到网络返回的输入流:InputStream is = conn.getInputStream();
6)将得到的文件流写出:outStream.write(buffer, 0, len);
向Internet发送请求参数
1)将地址和参数存到byte数组中:byte[] data = params.toString().getBytes();
2)创建URL对象:URL realUrl = new URL(requestUrl);
3)通过HttpURLConnection对象,向网络地址发送请求:HttpURLConnection conn = (HttpURLConnection) realUrl.openConnection();
4)设置容许输出:conn.setDoOutput(true);
5)设置不使用缓存:conn.setUseCaches(false);
6)设置使用POST的方式发送:conn.setRequestMethod("POST");
7)设置维持长连接:conn.setRequestProperty("Connection", "Keep-Alive");
8)设置文件字符集:conn.setRequestProperty("Charset", "UTF-8");
9)设置文件长度:conn.setRequestProperty("Content-Length", String.valueOf(data.length));
10)设置文件类型:conn.setRequestProperty("Content-Type","application/x-www-form-urlencoded");
11)以流的方式输出.
总结:
--发送POST请求必须设置允许输出。
--不要使用缓存,容易出现问题。
--在开始用HttpURLConnection对象的setRequestProperty()设置,就是生成HTML文件头。
第九讲:客户端Socket
1.Internet数据传输
1)Internet数据按有限大小的包传输,数据报(datagram)
2)数据报 = 首部(header)+有效载荷(payload)
首部:包括发送方的地址和端口、接收方的地址和端口、检测数据是否被破坏的检验和、保证可靠传输的其它管理信息。
有效载荷:数据本身。
3)需分解包、错误检测、数据重组、重传包、重排序等工作。
2.Socket
1)Socket封装了两个主机之间的一个连接
2)支持7个操作:
连接远程主机
发送数据
接收数据
关闭连接
绑定端口
监听入站数据
在绑定端口上接受来自远程机器的连接
3)Java的Socket类提供了前4个操作方法,客户端和服
务器都可以使用。
4)后3个操作,是等待客户端的连接,仅服务器需要,
由ServerSocket类实现。
Java的Socket实现
TCP协议:提供端到端面向连接的可靠的全双工字节流传输服务。
Java.net.Socket类: 连接远程主机、关闭连接;收、发数据:常见用法:创建Socket和Socket连接远程主机。
Java.net.ServerSocket类: 绑定、监听端口;处理端口上的连接请求;收、发数据。
Java程序如何使用客户端Socket
1)java程序用Socket构造函数创建一个新的Socket连接实例。
2)Socket尝试连接远程主机,连接成功就可从socket得到输入流和输出流。
3)Java程序使用输入流和输出流相互发送数据。
4)连接是全双工的:客户端和服务器端可以同时发送和接收数据。
5)传输的数据的含义取决于应用层协议。
6)先完成协商握手,再具体传输数据。
7) 数据传输结束后,关闭连接。
3.Telnet协议
1)工作在应用层的一个Internet协议。
2)Telnet
WinXP及之前默认开启;Win7及之后默认关闭;
如何开启Telnet:
控制面板 -> 程序和功能 -> 打开或关闭Windows功能 -> 勾选Telnet客户端
3)可以使用Telnet命令连接服务器
如:telnet www.szu.edu.cn 80
是否能够连通,取决于服务器是否支持telnet协议
用telnet命令方式从时间服务器读取数据
连接美国国家标准技术研究院(NIST)的时间服务器,请求当前时间:telnet time.nist.gov 13
服务器会以RFC867协议、人可读的格式返回结果;非标准时间服务协议。
4.日期格式
Daytime服务器发送的时间:
57516 16-05-08 12:48:05 50 0 0 105.0 UTC(NIST) *
读取此连接此服务器的socket的InputStream,可得到此结果。时间格式为:
JJJJJ YY-MM-DD HH:MM:SS TT L H msADV UTC(NIST) OTM
JJJJJ:修正儒略日(Modified Julian Date)自1858.11.17以来的日数
YY-MM-DD HH:MM:SS 日期与时间
TT:指示采用标准时间还是美国夏令时,50美国夏令时;00标准时间
L:一位码,指示当前月最后一天子夜是滞增加或减去一个闰秒。0表示无闰秒,1表示增加三个闰秒,2表示减去一个闰秒
H:服务器状态,0表示健康,1指最多相差的5秒,2指相差超过5S
msADV:延迟补偿毫秒数,NIST把此数增加到它发送的时间,对网络延迟做补偿
UTC:Coordinated Universal Time
5.使用socket编程方式从时间服务器获取数据
//Java7以上
try (Socket socket= new Socket(“time.nist.gov”,13)){
// 从socket读取……..
}catch(IOException ex){
System.err.println(“could not connect to time.nist.gov”);
}
//Java6及以前的版本需要
Socket socket=null;
try{
socket=new Socket(“time.nist.gov”,13);
// 从socket读取…
}catch (IOException ex){
System.err.prinln(ex);
}finally{
if (socket!=null){
try {
socket.close();
} catch (IOException e) { //doNothing
}
}
}
为socket连接设置超时时间
对socket设置一个超时时间,如果连接服务器间服务器挂起,则会给出socketTimeoutException异常通知.
import java.net.*;
import java.io.*;
public class DaytimeClient {
public static void main(String[] args) {
String hostname = args.length > 0 ? args[0] : "time.nist.gov";
Socket socket = null;
try {
socket = new Socket(hostname, 13);
socket.setSoTimeout(15000);//15秒
InputStream in = socket.getInputStream();
StringBuilder time = new StringBuilder();
InputStreamReader reader = new InputStreamReader(in, "ASCII");//从输入流读取服务器发送的字节,此处的协议指定发送的字节必须是ASCAII
for (int c = reader.read(); c != -1; c = reader.read()) {
time.append((char) c);
}
System.out.println(time);
} catch (IOException ex) {
System.err.println(ex);
} finally {
if (socket != null) {
try {
socket.close();
} catch (IOException ex) {
// ignore
}
}
}
}
}
使用协议和理解数据格式编程
网络程序的重点工作通常是使用协议和理解数据格式。
如果不只是显示输出时间服务器发送给你的文本,而是希望把它解析为一个java.util.Date对象,该如何编程?
通过与时间服务器time.nist.gov对话构造一个Date。
import java.net.*;
import java.text.*;
import java.util.Date;
import java.io.*;
public class Daytime {
public Date getDateFromNetwork() throws IOException, ParseException {
try (Socket socket = new Socket("time.nist.gov", 13)) {
socket.setSoTimeout(15000);
InputStream in = socket.getInputStream();
StringBuilder time = new StringBuilder();
InputStreamReader reader = new InputStreamReader(in, "ASCII");
for (int c = reader.read(); c != -1; c = reader.read()) {
time.append((char) c);
}
return parseDate(time.toString());
}
}
static Date parseDate(String s) throws ParseException {
String[] pieces = s.split(" ");
String dateTime = pieces[1] + " " + pieces[2] + " UTC";
DateFormat format = new SimpleDateFormat("yy-MM-dd hh:mm:ss z");
return format.parse(dateTime);
}
}
编程处理服务器返回的复杂数据格式
网络程序遇到不同的协议,可能会遇到陌生复杂的数据格式,那么程序必须读取原始字节,再按字节解释。
必须自己编写代码处理服务器发送的任意格式的数据。
import java.net.*;
import java.text.*;
import java.util.Date;
import java.io.*;
public class Time {
private static final String HOSTNAME = "time.nist.gov";
public static void main(String[] args) throws IOException, ParseException {
Date d = Time.getDateFromNetwork();
System.out.println("It is " + d);
}
public static Date getDateFromNetwork() throws IOException, ParseException {
// The time protocol sets the epoch at 1900,
// the Java Date class at 1970. This number
// converts between them.
long differenceBetweenEpochs = 2208988800L;
// If you'd rather not use the magic number, uncomment
// the following section which calculates it directly.
/*
TimeZone gmt = TimeZone.getTimeZone("GMT");
Calendar epoch1900 = Calendar.getInstance(gmt);
epoch1900.set(1900, 01, 01, 00, 00, 00);
long epoch1900ms = epoch1900.getTime().getTime();
Calendar epoch1970 = Calendar.getInstance(gmt);
epoch1970.set(1970, 01, 01, 00, 00, 00);
long epoch1970ms = epoch1970.getTime().getTime();
long differenceInMS = epoch1970ms - epoch1900ms;
long differenceBetweenEpochs = differenceInMS/1000;
*/
Socket socket = null;
try {
socket = new Socket(HOSTNAME, 37);
socket.setSoTimeout(15000);
InputStream raw = socket.getInputStream();
long secondsSince1900 = 0;
for (int i = 0; i < 4; i++) {
secondsSince1900 = (secondsSince1900 << 8) | raw.read();
}
long secondsSince1970
= secondsSince1900 - differenceBetweenEpochs;
long msSince1970 = secondsSince1970 * 1000;
Date time = new Date(msSince1970);
return time;
} finally {
try {
if (socket != null) socket.close();
}
catch (IOException ex) {}
}
}
}
6.用socket写入服务器
1)用Socket写入服务器,需向socket请求一个输出流和一个输入流,使用输出流在socket上发送数据,同时使用输入流读取数据。
2)使用RFC2229的字典(dict)协议:
telnet dict.org 2628
服务器正常,字典有变更,可以使用show db查看db,然后使用类似
define fd-eng-fra XXXX 来查询单词
编程实现该协议客户端:
Socket socket = new Socket(“dict.org”,2628)
OutputStream out = socket.getOutputStream();
Writer writer = new OutputStreamWriter(out, “UTF-8”);
writer.write(“DEFINEENG-latgold\r\n”);
writer.flush();
演示例子代码ex8-4,ch8.DictClient.java一个基于网络的英语-拉丁语翻译程序,dict协议应用的客户端实现。
import java.io.*;
import java.net.*;
public class DictClient {
public static final String SERVER = "dict.org";
public static final int PORT = 2628;
public static final int TIMEOUT = 15000;
public static void main(String[] args) {
Socket socket = null;
try {
socket = new Socket(SERVER, PORT);
socket.setSoTimeout(TIMEOUT);
OutputStream out = socket.getOutputStream();
Writer writer = new OutputStreamWriter(out, "UTF-8");
writer = new BufferedWriter(writer);
InputStream in = socket.getInputStream();
BufferedReader reader = new BufferedReader(
new InputStreamReader(in, "UTF-8"));
for (String word : args) {
define(word, writer, reader);
}
writer.write("quit\r\n");
writer.flush();
} catch (IOException ex) {
System.err.println(ex);
} finally { // dispose
if (socket != null) {
try {
socket.close();
} catch (IOException ex) {
// ignore
}
}
}
}
static void define(String word, Writer writer, BufferedReader reader)
throws IOException, UnsupportedEncodingException {
writer.write("DEFINE eng-lat " + word + "\r\n");
writer.flush();
for (String line = reader.readLine(); line != null; line = reader.readLine()) {
if (line.startsWith("250 ")) { // OK
return;
} else if (line.startsWith("552 ")) { // no match
System.out.println("No definition found for " + word);
return;
}
else if (line.matches("\\d\\d\\d .*")) continue;
else if (line.trim().equals(".")) continue;
else System.out.println(line);
}
}
}
7.半关闭socket
1)Close()方法同时关闭socket的输入和输出。
2)若只需要关闭连接的一半如输入或者输出,则可用:可以关闭一半的Socket
⚫ shutdownInput ()/ shutdownOutput()
⚫ 后续仍然需要close
3)半关闭方法实质并未关闭socket,只是调整与socket连接的流,使之认为已经到了流的末尾。半关闭方法并不释放与socket关联的资源,如端口等。关闭输入之后再读取输入流会返回-1,关闭输出之后再写入socket会抛出一个IOException异常。
4)即使半关闭了连接,或将连接的两半都关闭,使用结束后仍需关闭该socket。
5)若需确定输入流和输出流是打开的还是关闭的,可使用:Public boolean isInputShutdown()和Public boolean isOutputShutdown()。
8.构造和连接Socket
1)Java.net.socket类是java完成客户端TCP操作的基础类。使用原生代码与主机操作系统的本地TCP栈进行通信.
2)其他建立TCP连接的面向客户端的类,如URL、URLConnection、Applet和JEditorPane,最终都会调用这个类的方法:
基本构造函数
选择从哪个本地接口连接
构造但不连接
Socket地址
使用代理服务器
获取Socket的信息
关闭还是连接
3)基本构造函数:
public Socket(String host, int port) throws UnkownHostException, IOException
public Socket(InetAddress host, int port) throws IOException
第一个构造方法多了UnkownHostException的异常
利用构造函数创建Socket对象,连接远程主机Socket。端口扫描(攻防第一步,通常被防火墙阻拦、报告)
例子:LowPortScanner.java,检测指定主机上哪些端口开放,安装有TCP服务。
程序实现:查看指定主机上前1024个端口中哪些安装有TCP服务器(0-1023熟知端口号)
import java.net.*;
import java.io.*;
public class LowPortScanner {
public static void main(String[] args) {
String host = args.length > 0 ? args[0] : "localhost";
for (int i = 1; i < 1024; i++) {
try {
Socket s = new Socket(host, i);
System.out.println("There is a server on port " + i + " of "
+ host);
s.close();
} catch (UnknownHostException ex) {
System.err.println(ex);
break;
} catch (IOException ex) {
// must not be a server on this port
}
}
}
}
4)选择从哪个本地接口连接
public Socket(String host, int port, InetAddress interface, int localPort) throws IOException, UnknownHostException
public Socket(InetAddress host, int port, InetAddress interface, int localPort) throws IOException
前2个参数:远程主机;
后2个参数:本地接口和端口选择
本地接口接口和端口异常会抛出如BindException等IOException的子类
5)构造不连接
public Socket()
public void connect(SocketAddress endpoint, int timeout) throws IOException
先构造,以后再连接的编程方式:
Socket socket=new Socket();
SocketAddress address= new InetSocketAddress(SERVER, PORT);
try{
socket.connect(address);
// doSomething
}catch (IOException ex){ }//doSomething
finally{
try {
socket.close();
} catch (IOException ex) { … }
}
9.SocketAddress
1)当前只支持 TCP/IP Socket,都是InetSocketAddress的实例:
public SocketAddress getRemoteSocketAddress()
public SocketAddress getLocalSocketAddress()
2)用于socket.connect
3)也可使用InetSocketAddress直接构造
public InetSocketAddress(InetAddress address, int port)
public InetSocketAddress(String host, int port)
public InetSocketAddress(int port)
public static InetSocketAddress createUnresolved(String host, int port)
4)InetSocketAddress提供了getAddress、getPort、getHostName等方法
10.代理服务器
public Socket(Proxy proxy)
默认由socksProxyHost和socksProxyPort系统属性控制
可以通过前述函数自定义
Proxy.NO_PROXY,不使用代理
Proxy proxy = new Proxy(Proxy.Type.SOCKS, proxyAddress)
Proxy.Type.HTTP
Proxy.Type.DIRECT
11.获取Socket的信息与设置Socket选项
1)Socket对象有一些属性:
远程地址
远程端口
本地地址
本地端口
2)可以通过以下获取方法来访问:
public InetAddress getInetAddress()
public int getPort()
public InetAddress getLocalAddress()
public int getLocalPort()
4)关闭还是连接
判断一个socket是否连接到一个远程主机
public boolean isConnected()
返回true,socket确实能够连接远程主机,即使socket已关闭。
判断socket的状态:是否关闭
public boolean isClosed()
返回true,已关闭;false:未关闭,没连接过也会是false。
检查一个socket是否打开的正确方式:
boolean connected = socket.isConnected() && !socket.isClosed();
5)设置Socket选项
Socket选项指定 了java socket类所依赖的原生 socket如何发送和接收数据。客户端socket,java支持9个选项
TCP_NODELAY
设置TCP_NODELAY为true可确保包会尽可能快地发送
public void setTcpNoDelay(boolean on) throws SocketException
public Boolean getTcpNoDelay() throews SocketException
为了提高传输效率,Nagle算法进行了发送缓冲
在某些特定情况下不希望缓冲,Socket.setTcpNoDelay(true)
SO_LINGER
SO_LINGER选项指定了Socket关闭时如何处理尚未发送的数据报,默认情况下close()方法将立即返回,但系统仍会尝试发送剩余的数据。
public void setSoLinger(boolean on, int seconds) throws SocketException
public int getSoLinger() throws SocketException
0:不等,丢弃
正数:阻塞close,等n秒
getSoLinger等于-1表示还没设置过
SO_TIMEOUT
设置SO_TIMEOUT可以确保此次调用阻塞的时间不会超过某个固定的毫秒数。
public void setSoTimeout(int milliseconds) throws SocketException
public int getSoTimeout() throws SocketException
只要需要read,通常都要设置超时时间。
一次read超时,并不会关闭连接。
0意味着没有超时时间(连接正常时会一直等)。
SO_RCVBUF和SO_SNDBUF
接收和发送缓冲区的大小。
给底层实现的建议。
主要还是由TCP滑动窗口机制进行控制:太小不能有效利用网络;太大会阻塞网络。
SO_KEEPALIVE
public void setKeepAlive(boolean on) throws SocketException
public boolean getKeepAlive() throws SocketException
你还活着吗?
一般2小时一次,向空闲连接发送探测数据包,无响应则持续尝试11分钟(通常75秒一次),都没响应则关闭连接。
OOBINLINE
设置是否希望接收正常数据中的紧急数据。
默认情况下,java会忽略从socket接收的紧急数据:
public void setOOBInline(boolean on)throws SocketException
public boolean getOOBInline() throws SocketException
public void sendUrgentData(int data) throws IOEXception
收发紧急数据。
实际支持并不是很好。
用于在正常输入流中,插入有特异性的“紧急数据”,而且依赖高层应用程序的特别处理,例如“ctrl+c”。
SO_REUSEADDR
绑定端口会独占端口。
端口关闭时网络上仍然可能会传输发给旧端口的数据。
因此通常要等一小段时间端口才恢复可用。
SO_REUSEADDR,不等。
a.先构造socket但不连接(无参数构造函数)
b.再设置setReuseAddress(true)
c.再connect
d.之前的Socket也需要这样设置才能生效
IP_TOS服务类型
设置IP报头的服务类型字段。
12.SOCKET异常
1)IOException
SocketException
⚫BindException
✓ 通常是端口被占用,也可能是无权限使用该端口。
⚫ConnectException
✓ 连接被远程主机拒绝,通常是远程主机没在监听对应端口或进程忙。
⚫NoRouteToHostException
✓ 连接超时
2)ProtocolException
数据不符合 TCP/IP 规范。
第十讲:服务器Socket
1.使用ServerSocket
ServerSocket类包含了使用Java编写服务器所需的全部内容,服务器程序的基本生命周期:
1)构造ServerSocket(绑定某个端口)
2)accept方法监听(阻塞)并返回一个连接客户端的Socket
3)调用Socekt的getInputStream或getOutputStream方法获得IO流
4)根据已协商的协议交互
5)关闭连接
例子:持续监听!!
try(ServerSocket server=new ServerSocket(PORT)){
while(true){
try (Socket connection=server.accept()){
Writer out=new OutputStreamWriter(connection.getOutputStream());
Date now=new Date();
out.write(now.toString()+”\r\n”);
out.flush();
connection.close();
}catch (IOException ex){ }
}
}catch(IOExecption ex){…}
6)提供二进制数据, TimeServer时间服务器例子: 一个遵循RFC868时间协议的迭代时间服务器。当客户端连接时,服务器发送一个4字节大端无符号整数,指出从GMT1900.1.1 12:00A.M.后经过的秒数。
import java.io.*;
import java.net.*;
import java.util.Date;
public class TimeServer {
public final static int PORT = 37;
public static void main(String[] args) {
// The time protocol sets the epoch at 1900,
// the Date class at 1970. This number
// converts between them.
long differenceBetweenEpochs = 2208988800L;
try (ServerSocket server = new ServerSocket(PORT)) {
while (true) {
try (Socket connection = server.accept()) {
OutputStream out = connection.getOutputStream();
Date now = new Date();
long msSince1970 = now.getTime();
long secondsSince1970 = msSince1970/1000;
long secondsSince1900 = secondsSince1970
+ differenceBetweenEpochs;
byte[] time = new byte[4];
time[0]
= (byte) ((secondsSince1900 & 0x00000000FF000000L) >> 24);
time[1]
= (byte) ((secondsSince1900 & 0x0000000000FF0000L) >> 16);
time[2]
= (byte) ((secondsSince1900 & 0x000000000000FF00L) >> 8);
time[3] = (byte) (secondsSince1900 & 0x00000000000000FFL);
out.write(time);
out.flush();
} catch (IOException ex) {
System.err.println(ex.getMessage());
}
}
} catch (IOException ex) {
System.err.println(ex);
}
}
}
7)多线程服务器
1)待处理连接队列(队列长度有限)
2)进程、线程(避免异常阻塞)
MultithreadDaytimeServer多线程时间服务器例子
import java.net.*;
import java.io.*;
import java.util.Date;
public class MultithreadedDaytimeServer {
public final static int PORT = 13;
public static void main(String[] args) {
try (ServerSocket server = new ServerSocket(PORT)) {
while (true) {
try {
Socket connection = server.accept();
Thread task = new DaytimeThread(connection);
task.start();
} catch (IOException ex) {}
}
} catch (IOException ex) {
System.err.println("Couldn't start server");
}
}
private static class DaytimeThread extends Thread {
private Socket connection;
DaytimeThread(Socket connection) {
this.connection = connection;
}
@Override
public void run() {
try {
Writer out = new OutputStreamWriter(connection.getOutputStream());
Date now = new Date();
out.write(now.toString() +"\r\n");
out.flush();
} catch (IOException ex) {
System.err.println(ex);
} finally {
try {
connection.close();
} catch (IOException e) {
// ignore;
}
}
}
}
}
8)线程池
1)提高性能,避免崩溃
2)ExectorService
3)Callable< void >
PooledDaytimeServer使用线程池的时间服务器例子
import java.io.*;
import java.net.*;
import java.util.*;
import java.util.concurrent.*;
public class PooledDaytimeServer {
public final static int PORT = 13;
public static void main(String[] args) {
ExecutorService pool = Executors.newFixedThreadPool(50);
try (ServerSocket server = new ServerSocket(PORT)) {
while (true) {
try {
Socket connection = server.accept();
Callable<Void> task = new DaytimeTask(connection);
pool.submit(task);
} catch (IOException ex) {}
}
} catch (IOException ex) {
System.err.println("Couldn't start server");
}
}
private static class DaytimeTask implements Callable<Void> {
private Socket connection;
DaytimeTask(Socket connection) {
this.connection = connection;
}
@Override
public Void call() {
try {
Writer out = new OutputStreamWriter(connection.getOutputStream());
Date now = new Date();
out.write(now.toString() +"\r\n");
out.flush();
} catch (IOException ex) {
System.err.println(ex);
} finally {
try {
connection.close();
} catch (IOException e) {
// ignore;
}
}
return null;
}
}
}
9)用Socket写入服务器
echo服务器 ,ex9-5 EchoServer
需服务器端程序接受一个连接,同时获取该连接的输入流和输出流。
10) 关闭服务器Socket
注意区分关闭Socket与关闭ServerSocket
isClosed()与isBound()
isOpen:ss.isBound() && !ss.isClosed()
例子:EchoServer
Echo服务器,用socket写入服务器,客户端负责关闭连接,服务器会生成最多500个线程
import java.nio.*;
import java.nio.channels.*;
import java.net.*;
import java.util.*;
import java.io.IOException;
public class EchoServer {
public static int DEFAULT_PORT = 7;
public static void main(String[] args) {
int port;
try {
port = Integer.parseInt(args[0]);
} catch (RuntimeException ex) {
port = DEFAULT_PORT;
}
System.out.println("Listening for connections on port " + port);
ServerSocketChannel serverChannel;
Selector selector;
try {
serverChannel = ServerSocketChannel.open();
ServerSocket ss = serverChannel.socket();
InetSocketAddress address = new InetSocketAddress(port);
ss.bind(address);
serverChannel.configureBlocking(false);
selector = Selector.open();
serverChannel.register(selector, SelectionKey.OP_ACCEPT);
} catch (IOException ex) {
ex.printStackTrace();
return;
}
while (true) {
try {
selector.select();
} catch (IOException ex) {
ex.printStackTrace();
break;
}
Set<SelectionKey> readyKeys = selector.selectedKeys();
Iterator<SelectionKey> iterator = readyKeys.iterator();
while (iterator.hasNext()) {
SelectionKey key = iterator.next();
iterator.remove();
try {
if (key.isAcceptable()) {
ServerSocketChannel server = (ServerSocketChannel) key.channel();
SocketChannel client = server.accept();
System.out.println("Accepted connection from " + client);
client.configureBlocking(false);
SelectionKey clientKey = client.register(
selector, SelectionKey.OP_WRITE | SelectionKey.OP_READ);
ByteBuffer buffer = ByteBuffer.allocate(100);
clientKey.attach(buffer);
}
if (key.isReadable()) {
SocketChannel client = (SocketChannel) key.channel();
ByteBuffer output = (ByteBuffer) key.attachment();
client.read(output);
}
if (key.isWritable()) {
SocketChannel client = (SocketChannel) key.channel();
ByteBuffer output = (ByteBuffer) key.attachment();
output.flip();
client.write(output);
output.compact();
}
} catch (IOException ex) {
key.cancel();
try {
key.channel().close();
} catch (IOException cex) {}
}
}
}
}
}
2.日志
1)日志记录内容
请求。
服务器错误:错误日志一般要重点关注。
调试日志和生产日志。
2)如何记录日志
log4j
private final static Logger logger=Logger.getLogger(“requests”);
Logger.log(Level.SEVERE, “some msg” + ex. =getMessage(),ex)
Level:SEVERE\WARNING\INFO\CONFIG\FINE\FINER\FINEST
logger.info(new Date() + “ …” + someOtherUsefulMsg)
例子:LoggingDaytimeServer.java类实现记录请求和错误的daytime服务器,展示了如何为daytime服务器增加日志记录。
import java.io.*;
import java.net.*;
import java.util.Date;
import java.util.concurrent.*;
import java.util.logging.*;
public class LoggingDaytimeServer {
public final static int PORT = 13;
private final static Logger auditLogger = Logger.getLogger("requests");
private final static Logger errorLogger = Logger.getLogger("errors");
public static void main(String[] args) {
ExecutorService pool = Executors.newFixedThreadPool(50);
try (ServerSocket server = new ServerSocket(PORT)) {
while (true) {
try {
Socket connection = server.accept();
Callable<Void> task = new DaytimeTask(connection);
pool.submit(task);
} catch (IOException ex) {
errorLogger.log(Level.SEVERE, "accept error", ex);
} catch (RuntimeException ex) {
errorLogger.log(Level.SEVERE, "unexpected error " + ex.getMessage(), ex);
}
}
} catch (IOException ex) {
errorLogger.log(Level.SEVERE, "Couldn't start server", ex);
} catch (RuntimeException ex) {
errorLogger.log(Level.SEVERE, "Couldn't start server: " + ex.getMessage(), ex);
}
}
private static class DaytimeTask implements Callable<Void> {
private Socket connection;
DaytimeTask(Socket connection) {
this.connection = connection;
}
@Override
public Void call() {
try {
Date now = new Date();
// write the log entry first in case the client disconnects
auditLogger.info(now + " " + connection.getRemoteSocketAddress());
Writer out = new OutputStreamWriter(connection.getOutputStream());
out.write(now.toString() +"\r\n");
out.flush();
} catch (IOException ex) {
// client disconnected; ignore;
} finally {
try {
connection.close();
} catch (IOException ex) {
// ignore;
}
}
return null;
}
}
}
3.构造服务器Socket
1)4个构造函数
ServerSocket()
ServerSocket(int port)
ServerSocket(int port, int backlog)
ServerSocket(int port, int backlog, InetAddress bindAddr)
2)3个参数
监听端口,0表示自动分配
队列长度,>= 系统最大队列长度
绑定地址,多IP情况下的特定IP
4)利用BindException查找本地端口
5)构造但不绑定端口
用于在绑定前设置某些SO选项
void bind(SocketAddress endpoint)
void bind(SocketAddress endpoint, int backlog)
SocketAddress可为null,表示待系统分配
例子:LocalPortScanner.java 查找本地端口程序:尝试在各个端口创建ServerSocket对象,查看哪些端口失败,以此来检查本地机器的端口情况。
import java.io.*;
import java.net.*;
public class LocalPortScanner {
public static void main(String[] args) {
for (int port = 1; port <= 65535; port++) {
try {
// the next line will fail and drop into the catch block if
// there is already a server running on the port
ServerSocket server = new ServerSocket(port);
} catch (IOException ex) {
System.out.println("There is a server on port " + port + ".");
}
}
}
}
4.获得服务器Socket的有关信息
InetAddress getInetAddress()
int getLocalPort()
SocketAddress getLocalSocketAddress()
例子:RandomPort.java 随机端口程序:ServerSocket构造函数允许为端口号传递0,监听未指定的端口,利用此方法可以找出所监听的端口。
import java.io.*;
import java.net.*;
public class RandomPort {
public static void main(String[] args) {
try {
ServerSocket server = new ServerSocket(0);
System.out.println("This server runs on port "
+ server.getLocalPort());
} catch (IOException ex) {
System.err.println(ex);
}
}
}
5.Socket选项
1)Socket选项指定了ServerSocket类所依赖的原生Socket如何发送和接收数据,支持3个选项:
2)SO_TIMEOUT:默认0表示永不超时;自调用accept()后开始计时(阻塞等待时间)
3)SO_REUSEADDR:与客户端Socket的同名选项作用相同
4) SO_RCVBUF: 设置的是哪个缓冲区的大小?
5) 服务类型:TCP定义4个通用业务流类型:高可靠性、低成本、最大吞吐量、最小延迟
setPerformancePreferences(int connectionTime, int latency, int bandwidth),数值越高越重要
6.HTTP服务器
1)简单HTTP服务器用途
小且简单网站,定制服务器,节约资源。
仅处理一件事情,高效。如一台专门服务器将常用文件存到RAM中,专门的重定向服务器(也可用域名解析来解决这个问题)。
2) Java VS C/C++
HTTP服务器的瓶颈在带宽等I/O问题;CPU计算能力一般不是瓶颈; Java优势:内存保护、垃圾自动回收。
3)单文件HTTP服务器
无论什么请求,服务器始终发送同一个文件
过程:
main:创建SingleFileHTTPServer对象--->创建线程池--->无限循环--->有客户端连接--->交由Handler对象处理
例子:SingleFileHTTPServer.java提供同一个文件的HTTP服务器。
import java.io.*;
import java.net.*;
import java.nio.charset.Charset;
import java.nio.file.*;
import java.util.concurrent.*;
import java.util.logging.*;
public class SingleFileHTTPServer {
private static final Logger logger = Logger.getLogger("SingleFileHTTPServer");
private final byte[] content;
private final byte[] header;
private final int port;
private final String encoding;
public SingleFileHTTPServer(String data, String encoding,
String mimeType, int port) throws UnsupportedEncodingException {
this(data.getBytes(encoding), encoding, mimeType, port);
}
public SingleFileHTTPServer(
byte[] data, String encoding, String mimeType, int port) {
this.content = data;
this.port = port;
this.encoding = encoding;
String header = "HTTP/1.0 200 OK\r\n"
+ "Server: OneFile 2.0\r\n"
+ "Content-length: " + this.content.length + "\r\n"
+ "Content-type: " + mimeType + "; charset=" + encoding + "\r\n\r\n";
this.header = header.getBytes(Charset.forName("US-ASCII"));
}
public void start() {
ExecutorService pool = Executors.newFixedThreadPool(100);
try (ServerSocket server = new ServerSocket(this.port)) {
logger.info("Accepting connections on port " + server.getLocalPort());
logger.info("Data to be sent:");
logger.info(new String(this.content, encoding));
while (true) {
try {
Socket connection = server.accept();
pool.submit(new HTTPHandler(connection));
} catch (IOException ex) {
logger.log(Level.WARNING, "Exception accepting connection", ex);
} catch (RuntimeException ex) {
logger.log(Level.SEVERE, "Unexpected error", ex);
}
}
} catch (IOException ex) {
logger.log(Level.SEVERE, "Could not start server", ex);
}
}
private class HTTPHandler implements Callable<Void> {
private final Socket connection;
HTTPHandler(Socket connection) {
this.connection = connection;
}
@Override
public Void call() throws IOException {
try {
OutputStream out = new BufferedOutputStream(
connection.getOutputStream()
);
InputStream in = new BufferedInputStream(
connection.getInputStream()
);
// read the first line only; that's all we need
StringBuilder request = new StringBuilder(80);
while (true) {
int c = in.read();
if (c == '\r' || c == '\n' || c == -1) break;
request.append((char) c);
}
// If this is HTTP/1.0 or later send a MIME header
if (request.toString().indexOf("HTTP/") != -1) {
out.write(header);
}
out.write(content);
out.flush();
} catch (IOException ex) {
logger.log(Level.WARNING, "Error writing to client", ex);
} finally {
connection.close();
}
return null;
}
}
public static void main(String[] args) {
// set the port to listen on
int port;
try {
port = Integer.parseInt(args[1]);
if (port < 1 || port > 65535) port = 80;
} catch (RuntimeException ex) {
port = 80;
}
String encoding = "UTF-8";
if (args.length > 2) encoding = args[2];
try {
Path path = Paths.get(args[0]);;
byte[] data = Files.readAllBytes(path);
String contentType = URLConnection.getFileNameMap().getContentTypeFor(args[0]);
SingleFileHTTPServer server = new SingleFileHTTPServer(data, encoding,
contentType, port);
server.start();
} catch (ArrayIndexOutOfBoundsException ex) {
System.out.println(
"Usage: java SingleFileHTTPServer filename port encoding");
} catch (IOException ex) {
logger.severe(ex.getMessage());
}
}
}
4)重定向HTTP服务器
将请求重定向到一个新网站
客户端:GET /nc/ HTTP/1.1
例子:Redirector.java,HTTP重定向器,一个简单有用的HTTP服务器
import java.io.*;
import java.net.*;
import java.util.*;
import java.util.logging.*;
public class Redirector {
private static final Logger logger = Logger.getLogger("Redirector");
private final int port;
private final String newSite;
public Redirector(String newSite, int port) {
this.port = port;
this.newSite = newSite;
}
public void start() {
try (ServerSocket server = new ServerSocket(port)) {
logger.info("Redirecting connections on port "
+ server.getLocalPort() + " to " + newSite);
while (true) {
try {
Socket s = server.accept();
Thread t = new RedirectThread(s);
t.start();
} catch (IOException ex) {
logger.warning("Exception accepting connection");
} catch (RuntimeException ex) {
logger.log(Level.SEVERE, "Unexpected error", ex);
}
}
} catch (BindException ex) {
logger.log(Level.SEVERE, "Could not start server.", ex);
} catch (IOException ex) {
logger.log(Level.SEVERE, "Error opening server socket", ex);
}
}
private class RedirectThread extends Thread {
private final Socket connection;
RedirectThread(Socket s) {
this.connection = s;
}
public void run() {
try {
Writer out = new BufferedWriter(
new OutputStreamWriter(
connection.getOutputStream(), "US-ASCII"
)
);
Reader in = new InputStreamReader(
new BufferedInputStream(
connection.getInputStream()
)
);
// read the first line only; that's all we need
StringBuilder request = new StringBuilder(80);
while (true) {
int c = in.read();
if (c == '\r' || c == '\n' || c == -1) break;
request.append((char) c);
}
String get = request.toString();
String[] pieces = get.split("\\w*");
String theFile = pieces[1];
// If this is HTTP/1.0 or later send a MIME header
if (get.indexOf("HTTP") != -1) {
out.write("HTTP/1.0 302 FOUND\r\n");
Date now = new Date();
out.write("Date: " + now + "\r\n");
out.write("Server: Redirector 1.1\r\n");
out.write("Location: " + newSite + theFile + "\r\n");
out.write("Content-type: text/html\r\n\r\n");
out.flush();
}
// Not all browsers support redirection so we need to
// produce HTML that says where the document has moved to.
out.write("<HTML><HEAD><TITLE>Document moved</TITLE></HEAD>\r\n");
out.write("<BODY><H1>Document moved</H1>\r\n");
out.write("The document " + theFile
+ " has moved to\r\n<A HREF=\"" + newSite + theFile + "\">"
+ newSite + theFile
+ "</A>.\r\n Please update your bookmarks<P>");
out.write("</BODY></HTML>\r\n");
out.flush();
logger.log(Level.INFO,
"Redirected " + connection.getRemoteSocketAddress());
} catch(IOException ex) {
logger.log(Level.WARNING,
"Error talking to " + connection.getRemoteSocketAddress(), ex);
} finally {
try {
connection.close();
} catch (IOException ex) {}
}
}
}
public static void main(String[] args) {
int thePort;
String theSite;
try {
theSite = args[0];
// trim trailing slash
if (theSite.endsWith("/")) {
theSite = theSite.substring(0, theSite.length() - 1);
}
} catch (RuntimeException ex) {
System.out.println(
"Usage: java Redirector http://www.newsite.com/ port");
return;
}
try {
thePort = Integer.parseInt(args[1]);
} catch (RuntimeException ex) {
thePort = 80;
}
Redirector redirector = new Redirector(theSite, thePort);
redirector.start();
}
}
5)功能完备的HTTP服务器
流程:
main()获取主目录、端口参数--->创建线程池pool--->每个客户端,一个线程处理--->ExecutorService.submit()-->后去请求的文件路径--->把文件发给客户端
例子:JHTTP.java实现一个基本的web服务器主类RequestProcessor.java处理HTTP请求的线程类
import java.io.*;
import java.net.*;
import java.util.concurrent.*;
import java.util.logging.*;
public class JHTTP {
private static final Logger logger = Logger.getLogger(
JHTTP.class.getCanonicalName());
private static final int NUM_THREADS = 50;
private static final String INDEX_FILE = "index.html";
private final File rootDirectory;
private final int port;
public JHTTP(File rootDirectory, int port) throws IOException {
if (!rootDirectory.isDirectory()) {
throw new IOException(rootDirectory
+ " does not exist as a directory");
}
this.rootDirectory = rootDirectory;
this.port = port;
}
public void start() throws IOException {
ExecutorService pool = Executors.newFixedThreadPool(NUM_THREADS);
try (ServerSocket server = new ServerSocket(port)) {
logger.info("Accepting connections on port " + server.getLocalPort());
logger.info("Document Root: " + rootDirectory);
while (true) {
try {
Socket request = server.accept();
Runnable r = new RequestProcessor(
rootDirectory, INDEX_FILE, request);
pool.submit(r);
} catch (IOException ex) {
logger.log(Level.WARNING, "Error accepting connection", ex);
}
}
}
}
public static void main(String[] args) {
// get the Document root
File docroot;
try {
docroot = new File(args[0]);
} catch (ArrayIndexOutOfBoundsException ex) {
System.out.println("Usage: java JHTTP docroot port");
return;
}
// set the port to listen on
int port;
try {
port = Integer.parseInt(args[1]);
if (port < 0 || port > 65535) port = 80;
} catch (RuntimeException ex) {
port = 80;
}
try {
JHTTP webserver = new JHTTP(docroot, port);
webserver.start();
} catch (IOException ex) {
logger.log(Level.SEVERE, "Server could not start", ex);
}
}
}
例子:RequestProcessor.java处理HTTP请求的线程类
import java.io.*;
import java.net.*;
import java.nio.file.Files;
import java.util.*;
import java.util.logging.*;
public class RequestProcessor implements Runnable {
private final static Logger logger = Logger.getLogger(
RequestProcessor.class.getCanonicalName());
private File rootDirectory;
private String indexFileName = "index.html";
private Socket connection;
public RequestProcessor(File rootDirectory,
String indexFileName, Socket connection) {
if (rootDirectory.isFile()) {
throw new IllegalArgumentException(
"rootDirectory must be a directory, not a file");
}
try {
rootDirectory = rootDirectory.getCanonicalFile();
} catch (IOException ex) {
}
this.rootDirectory = rootDirectory;
if (indexFileName != null) this.indexFileName = indexFileName;
this.connection = connection;
}
@Override
public void run() {
// for security checks
String root = rootDirectory.getPath();
try {
OutputStream raw = new BufferedOutputStream(
connection.getOutputStream()
);
Writer out = new OutputStreamWriter(raw);
Reader in = new InputStreamReader(
new BufferedInputStream(
connection.getInputStream()
),"US-ASCII"
);
StringBuilder requestLine = new StringBuilder();
while (true) {
int c = in.read();
if (c == '\r' || c == '\n') break;
requestLine.append((char) c);
}
String get = requestLine.toString();
logger.info(connection.getRemoteSocketAddress() + " " + get);
String[] tokens = get.split("\\s+");
String method = tokens[0];
String version = "";
if (method.equals("GET")) {
String fileName = tokens[1];
if (fileName.endsWith("/")) fileName += indexFileName;
String contentType =
URLConnection.getFileNameMap().getContentTypeFor(fileName);
if (tokens.length > 2) {
version = tokens[2];
}
File theFile = new File(rootDirectory,
fileName.substring(1, fileName.length()));
if (theFile.canRead()
// Don't let clients outside the document root
&& theFile.getCanonicalPath().startsWith(root)) {
byte[] theData = Files.readAllBytes(theFile.toPath());
if (version.startsWith("HTTP/")) { // send a MIME header
sendHeader(out, "HTTP/1.0 200 OK", contentType, theData.length);
}
// send the file; it may be an image or other binary data
// so use the underlying output stream
// instead of the writer
raw.write(theData);
raw.flush();
} else { // can't find the file
String body = new StringBuilder("<HTML>\r\n")
.append("<HEAD><TITLE>File Not Found</TITLE>\r\n")
.append("</HEAD>\r\n")
.append("<BODY>")
.append("<H1>HTTP Error 404: File Not Found</H1>\r\n")
.append("</BODY></HTML>\r\n").toString();
if (version.startsWith("HTTP/")) { // send a MIME header
sendHeader(out, "HTTP/1.0 404 File Not Found",
"text/html; charset=utf-8", body.length());
}
out.write(body);
out.flush();
}
} else { // method does not equal "GET"
String body = new StringBuilder("<HTML>\r\n")
.append("<HEAD><TITLE>Not Implemented</TITLE>\r\n")
.append("</HEAD>\r\n")
.append("<BODY>")
.append("<H1>HTTP Error 501: Not Implemented</H1>\r\n")
.append("</BODY></HTML>\r\n").toString();
if (version.startsWith("HTTP/")) { // send a MIME header
sendHeader(out, "HTTP/1.0 501 Not Implemented",
"text/html; charset=utf-8", body.length());
}
out.write(body);
out.flush();
}
} catch (IOException ex) {
logger.log(Level.WARNING,
"Error talking to " + connection.getRemoteSocketAddress(), ex);
} finally {
try {
connection.close();
}
catch (IOException ex) {}
}
}
private void sendHeader(Writer out, String responseCode,
String contentType, int length)
throws IOException {
out.write(responseCode + "\r\n");
Date now = new Date();
out.write("Date: " + now + "\r\n");
out.write("Server: JHTTP 2.0\r\n");
out.write("Content-length: " + length + "\r\n");
out.write("Content-type: " + contentType + "\r\n\r\n");
out.flush();
}
}
第十一讲:安全Socket
1.保护Internet通信
1)加密
明文、密钥、加密算法、密文
对称加密(秘密密钥)
非对称加密(公开密钥)
⚫ 用于身份认证
⚫ 用于消息完整性检查
二次加密
⚫ 自己私钥、他人公钥
2)中间人攻击
通过可信第三方认证机构存储和验证公开密钥
3)Java安全socket扩展有四个包:
javax.net.ssl.SSLSocket:安全socketAPI抽象类
javax.net.ssl.SSLSocketFactory:创建安全Socket的抽象工厂
java.security.cert:处理SSL需公开秘钥证书的类
com.sun.net.ssl:具体实现类
2.创建安全客户端Socket
SSLSocketyFactory factory=(SSLSocketFactory)SSLSocketFactory.getDefault();
SSLSocket socket=null;
try{
socket=(SSLSocket) factory.creatSocket(host, port); //通常port=443
String[] supported=socket.getSupportCipherSuites();
Socket.setEnabledCipherSuites(supported);
// get I/O stream and read write as usual,
……
}
1)安全socket对象生产类SSLSocketFacotory
static SocketFactory getDefault()
继承自javax.net.SocketFactory
➢ Socket createSocket()
➢ abstract Socket createSocket(InetAddress host, int port)
➢ abstract Socket createSocket(InetAddress address, int port, InetAddress localAddress, int localPort)
➢ abstract Socket createSocket(String host, int port)
➢ abstract Socket createSocket(String host, int port, InetAddress localHost, intlocalPort)
例子:使用安全Socket的客户端例子ex10-1 HTTPSClient,程序使用安全socket实现连接一个安全服务器(美国邮政服务www.usps.com),发送简单的GET请求并响应。
import java.io.*;
import javax.net.ssl.*;
public class HTTPSClient {
public static void main(String[] args) {
if (args.length == 0) {
System.out.println("Usage: java HTTPSClient2 host");
return;
}
int port = 443; // default https port
String host = args[0];
SSLSocketFactory factory
= (SSLSocketFactory) SSLSocketFactory.getDefault();
SSLSocket socket = null;
try {
socket = (SSLSocket) factory.createSocket(host, port);
// enable all the suites
String[] supported = socket.getSupportedCipherSuites();
socket.setEnabledCipherSuites(supported);
Writer out = new OutputStreamWriter(socket.getOutputStream(), "UTF-8");
// https requires the full URL in the GET line
out.write("GET http://" + host + "/ HTTP/1.1\r\n");
out.write("Host: " + host + "\r\n");
out.write("\r\n");
out.flush();
// read response
BufferedReader in = new BufferedReader(
new InputStreamReader(socket.getInputStream()));
// read the header
String s;
while (!(s = in.readLine()).equals("")) {
System.out.println(s);
}
System.out.println();
// read the length
String contentLength = in.readLine();
int length = Integer.MAX_VALUE;
try {
length = Integer.parseInt(contentLength.trim(), 16);
} catch (NumberFormatException ex) {
// This server doesn't send the content-length
// in the first line of the response body
}
System.out.println(contentLength);
int c;
int i = 0;
while ((c = in.read()) != -1 && i++ < length) {
System.out.write(c);
}
System.out.println();
} catch (IOException ex) {
System.err.println(ex);
} finally {
try {
if (socket != null) socket.close();
} catch (IOException e) {}
}
}
}
2)选择密码组
SSLSocketFactory类中的方法:
abstract String[] getSupportedCipherSuites()
指出给定Socket上可用的算法组合。
abstract void setEnabledCipherSuites(String[] suites)
修改客户端试图使用的密码组。
JDK1.7以上支持多个密码组,每个密码组名包括4个部分(协议、密钥交换算法、加密算法和校验和):
TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256
TLS(Transport Layer Security)与SSL(Secure Socket Layer)协议:TLS 是IETF接手SSL后制定的后续版本(国际标准)
ECDHE:Elliptic Curve Diffie-Hellman Exchange,基于ECC(Elliptic Curve Cryptography,椭圆曲线密码学,类似RSA)的密钥交换算法(在不安全的信道上安全的交换秘钥)
ECDSA:椭圆曲线数字签名算法
AES_128_CBC:Advanced Encryption Standard,DES的后续,128位,采用CBC(Cipher Block Chaining,密码分组链接模式)
SHA256:Secure Hash Algorithm,安全散列算法,类似MD5的消息摘要算法
3)客户端模式
安全Internet通信要求服务器证书认证自己,客户端通常不需要认证自己,若要求客户端需要认证自己,可使用:
setUseClientMode(boolean mode)
确定socket是否需要在第一次握手时使用认证。
如果服务端要求与它连接的所有客户端都需要认证,可以通过:
public abstract void setNeedClientAuth(boolean needsAuthentication)来进行设置。
4)创建安全服务器Socket步骤
-
使用keytool生成公开密钥和证书;
-
请第三方机构认证证书;
-
为算法创建一个SSLContext;
-
为证书源创建TrustManagerFactory;
-
为密钥类型创建KeyManagerFactory;
-
为密钥和证书数据创建一个KeyStore对象;
-
用密钥和证书填充KeyStore对象;
-
用KeyStore及其口令短语初始化KeyManagerFactory
-
用KeyManagerFactory中的密钥管理器、TrustManagerFactory中的信任管理器和一个随机源初始化上下文。
SSLServerSocket与SSLSocket类似,通过SSLServerSocketFactory创建
SSLServerSocket (Java Platform SE 8 )
例子:使用安全服务器Socket例子ex10-2 SecureOrderTaker,程序接受订单并显示在控制台输出。需先使用JDK提供的keytool程序生成jnp4e.keys文件,keystore的口令为2andnotafnordKeytool –genkey –alias ourstore –keystore jnp4e.keys
import java.io.*;
import java.net.*;
import java.security.*;
import java.security.cert.CertificateException;
import java.util.Arrays;
import javax.net.ssl.*;
public class SecureOrderTaker {
public final static int PORT = 7000;
public final static String algorithm = "SSL";
public static void main(String[] args) {
try {
SSLContext context = SSLContext.getInstance(algorithm);
// The reference implementation only supports X.509 keys
KeyManagerFactory kmf = KeyManagerFactory.getInstance("SunX509");
// Oracle's default kind of key store
KeyStore ks = KeyStore.getInstance("JKS");
// For security, every key store is encrypted with a
// passphrase that must be provided before we can load
// it from disk. The passphrase is stored as a char[] array
// so it can be wiped from memory quickly rather than
// waiting for a garbage collector.
char[] password = System.console().readPassword();
ks.load(new FileInputStream("jnp4e.keys"), password);
kmf.init(ks, password);
context.init(kmf.getKeyManagers(), null, null);
// wipe the password
Arrays.fill(password, '0');
SSLServerSocketFactory factory
= context.getServerSocketFactory();
SSLServerSocket server
= (SSLServerSocket) factory.createServerSocket(PORT);
// add anonymous (non-authenticated) cipher suites
String[] supported = server.getSupportedCipherSuites();
String[] anonCipherSuitesSupported = new String[supported.length];
int numAnonCipherSuitesSupported = 0;
for (int i = 0; i < supported.length; i++) {
if (supported[i].indexOf("_anon_") > 0) {
anonCipherSuitesSupported[numAnonCipherSuitesSupported++] =
supported[i];
}
}
String[] oldEnabled = server.getEnabledCipherSuites();
String[] newEnabled = new String[oldEnabled.length
+ numAnonCipherSuitesSupported];
System.arraycopy(oldEnabled, 0, newEnabled, 0, oldEnabled.length);
System.arraycopy(anonCipherSuitesSupported, 0, newEnabled,
oldEnabled.length, numAnonCipherSuitesSupported);
server.setEnabledCipherSuites(newEnabled);
// Now all the set up is complete and we can focus
// on the actual communication.
while (true) {
// This socket will be secure,
// but there's no indication of that in the code!
try (Socket theConnection = server.accept()) {
InputStream in = theConnection.getInputStream();
int c;
while ((c = in.read()) != -1) {
System.out.write(c);
}
} catch (IOException ex) {
ex.printStackTrace();
}
}
} catch (IOException | KeyManagementException
| KeyStoreException | NoSuchAlgorithmException
| CertificateException | UnrecoverableKeyException ex) {
ex.printStackTrace();
}
}
}
3.配置SSLServerSocket
SSLServerSocket类也提供了相应的方法支持下面的设置。
1)选择密码组
Public abstract String[] getSupportedSuites()
Public abstract String[] getEnabledCipherSuites()
Public abstract void setEnableedCipherSuites(String[] suites)
2)会话管理
Public abstract void setEnableSessiionCreation(Boolean allowSessions)
Public abstract Boolean getEnableSessionCreation()
3)客户端模式
确定和指定是否要求客户端向服务器认证自己。
Public abstract void setNeedClientAuth(Boolean flag)
Public abstract Boolean getNeedClientAuth()
第十二讲:非阻塞IO
1.概述
1)非阻塞I/O
字节流InputStream、OutputStream的阻塞I/O
非阻塞是不是一定比阻塞好?
2)允许CPU速度高于网络的方案:
缓冲+多线程:线程切换;多核。
3)NIO的优劣势
优势:适用于维持大量长期但不非常活跃的连接,如推送服务的后台。
劣势:但增加了代码复杂度:不要贸然优化。
4)Java.nio包
2.一个基于通道的chargen客户端示例
1)通道Channel
类似Stream,通过它向数据源读(或目的地写)数据块(Buffer)
区别:Stream读写的是一个个字节,Channel读写的是一个个数据块
提供I/O异步支持
2)SocketChannel
SocketChannel (Java Platform SE 8 )
3) 缓冲区Buffer
通常是一个字节数组 + 几个Index(mark<=position<= limit<=capacity)
相对和绝对的get、put
flip & rewind
4)ChargenClient chargen(无限循环)协议客户端。Ctrl-C结束程序。 协议内容:服务器在端口19监听连接,当客户端连接时,服务器发送连续的字符序列,直到客户端断开连接为止。客户端所有输入都被忽略。以回车/换行作为行分隔符的72字符循环文本行 。
import java.nio.*;
import java.nio.channels.*;
import java.net.*;
import java.io.IOException;
public class ChargenClient {
public static int DEFAULT_PORT = 19;
public static void main(String[] args) {
if (args.length == 0) {
System.out.println("Usage: java ChargenClient host [port]");
return;
}
int port;
try {
port = Integer.parseInt(args[1]);
} catch (RuntimeException ex) {
port = DEFAULT_PORT;
}
try {
SocketAddress address = new InetSocketAddress(args[0], port);
SocketChannel client = SocketChannel.open(address);
ByteBuffer buffer = ByteBuffer.allocate(74);
WritableByteChannel out = Channels.newChannel(System.out);
while (client.read(buffer) != -1) {
buffer.flip();
out.write(buffer);
buffer.clear();
}
} catch (IOException ex) {
ex.printStackTrace();
}
}
}
3.一个非阻塞的chargen服务器示例
1)选择器Selector
作用于多个SelectableChannel上的一个控制选择器
SelectableChannel向选择器注册(register)时得到的指针,选择器工作(选择)时,将返回符合条件的这些指针的集合。选择器维持:key set、selected-key set和cancelled-key set三个集合。selected-key中的key,通常处理后需要remove(通过set或iterator)。
选择:阻塞型选择select(), select(long)和非阻塞型选择selectNow() 。查询操作系统,询问已注册的这些Channel,他们感兴趣的事情是否已经发生、就绪;若是把对应的key加入selected-key set中。
并发:选择器本身是线程安全的,但三个key set不是
Selector (Java Platform SE 8 )
2)创建ServerSocketChannel
--->获得ServerSocket并绑定端口
--->(可选)服务端非阻塞的accept模式:
配置serverChannel采用非阻塞模式
构造selector并进行accept ready的注册
selector开始工作(select),选出已经accept的channel的key
对选出来的accept ready的key,获得channel,accept得到SocketChannel,然后配置buffer,进行非阻塞的IO
特别的remove处理
import java.nio.*;
import java.nio.channels.*;
import java.net.*;
import java.util.*;
import java.io.IOException;
public class ChargenServer {
public static int DEFAULT_PORT = 19;
public static void main(String[] args) {
int port;
try {
port = Integer.parseInt(args[0]);
} catch (RuntimeException ex) {
port = DEFAULT_PORT;
}
System.out.println("Listening for connections on port " + port);
byte[] rotation = new byte[95*2];
for (byte i = ' '; i <= '~'; i++) {
rotation[i -' '] = i;
rotation[i + 95 - ' '] = i;
}
ServerSocketChannel serverChannel;
Selector selector;
try {
serverChannel = ServerSocketChannel.open();
ServerSocket ss = serverChannel.socket();
InetSocketAddress address = new InetSocketAddress(port);
ss.bind(address);
serverChannel.configureBlocking(false);
selector = Selector.open();
serverChannel.register(selector, SelectionKey.OP_ACCEPT);
} catch (IOException ex) {
ex.printStackTrace();
return;
}
while (true) {
try {
selector.select();
} catch (IOException ex) {
ex.printStackTrace();
break;
}
Set<SelectionKey> readyKeys = selector.selectedKeys();
Iterator<SelectionKey> iterator = readyKeys.iterator();
while (iterator.hasNext()) {
SelectionKey key = iterator.next();
iterator.remove();
try {
if (key.isAcceptable()) {
ServerSocketChannel server = (ServerSocketChannel) key.channel();
SocketChannel client = server.accept();
System.out.println("Accepted connection from " + client);
client.configureBlocking(false);
SelectionKey key2 = client.register(selector, SelectionKey.
OP_WRITE);
ByteBuffer buffer = ByteBuffer.allocate(74);
buffer.put(rotation, 0, 72);
buffer.put((byte) '\r');
buffer.put((byte) '\n');
buffer.flip();
key2.attach(buffer);
} else if (key.isWritable()) {
SocketChannel client = (SocketChannel) key.channel();
ByteBuffer buffer = (ByteBuffer) key.attachment();
if (!buffer.hasRemaining()) {
// Refill the buffer with the next line
buffer.rewind();
// Get the old first character
int first = buffer.get();
// Get ready to change the data in the buffer
buffer.rewind();
// Find the new first characters position in rotation
int position = first - ' ' + 1;
// copy the data from rotation into the buffer
buffer.put(rotation, position, 72);
// Store a line break at the end of the buffer
buffer.put((byte) '\r');
buffer.put((byte) '\n');
// Prepare the buffer for writing
buffer.flip();
}
client.write(buffer);
}
} catch (IOException ex) {
key.cancel();
try {
key.channel().close();
}
catch (IOException cex) {}
}
}
}
}
}
4.缓冲区
1)创建缓冲区
分配; 直接分配;包装。
2)填充和排空
3)批量方法
4)数据转换
5)视图缓冲区
6)压缩缓冲区 (剩余数据移到开头)
7)复制缓冲区
8)分片缓冲区
9)标记和重置
10)Object方法
5.通道
1)Channels类是一个工具类,可将基于I/O的流、阅读器和书写器包装在通道中,也可以从通道转换为基于I/O的流、阅读器和书写器。
2) SocketChannel类实现了Channels类中的Readable(Writable)ByteChannel接口。
连接;读取;写入;关闭。
3)ServerSocketChannel
创建服务器Socket通道:接受连接。
4)异步通道(Java7及以上)AsynchronousSocketChannel类和AsynchronoousServerSocketChannel类
5.就绪选择
1)即能够选择读写时不阻塞的socket
2)进行就绪选择,要将不同的通道注册到一个selector对象,每个通道分配一个selectionKey,需:
Selector类和 SelectionKey类
第十三讲:UDP&IP组播
1.UDP与TCP
TCP:保证数据到达;保证数据顺序;速度慢;用途:HTTP、FTP、…
UDP:不保证一定到达;不保证到达顺序;速度快;用途:DNS、NFS、TFTP、…
TCP协议:ServerSocket&Socket
UDP协议:没有连接(connection)概念;不支持流(stream);只是数据包的发送和接收。
DatagramPacket:对数据进行包装
DatagramSocket:发送和接收数据
2.UDP客户端
1)创建DatagramSocket对象、并设置超时
DatagramSocket socket = new DatagramSocket(0);
socket.setSoTimeout(10000); //10s
2)设置要发送DatagramPacket数据包
InetAddress host = InetAddress.getByName("time.nist.gov");
DatagramPacket request = new DatagramPacket(new byte[1], 1, host, 13);
3)创建要接收的DatagramPacket数据包
byte[] data = new byte[1024];
DatagramPacket response = new DatagramPacket(data, data.length);
4)发送和接收数据
socket.send(request);
socket.receive(response);
5) 解析出数据
byte [] b = response.getData();
import java.io.*;
import java.net.*;
public class DaytimeUDPClient {
private final static int PORT = 13;
private static final String HOSTNAME = "time.nist.gov";
public static void main(String[] args) {
try (DatagramSocket socket = new DatagramSocket(0)) {
socket.setSoTimeout(10000);
InetAddress host = InetAddress.getByName(HOSTNAME);
DatagramPacket request = new DatagramPacket(new byte[1], 1, host , PORT);
DatagramPacket response = new DatagramPacket(new byte[1024], 1024);
socket.send(request);
socket.receive(response);
String result = new String(response.getData(), 0, response.getLength(),
"US-ASCII");
System.out.println(result);
} catch (IOException ex) {
ex.printStackTrace();
}
}
}
3.UDP服务器
实现方法跟UDP客户端类似
1)创建DatagramSocket对象
DatagramSocket socket = new DatagramSocket(13);
2)创建要接收数据的DatagramPacket对象
DatagramPacket request = new DatagramPacket(new byte[1024], 0, 1024);
3)接收数据
socket.receive(request)
4)设置要发送的DatagramPacket数据包
byte[] data = ...;
DatagramPacket response = new DatagramPacket(data, data.length, host, port);
5)发送数据
socket.send(response);
import java.net.*;
import java.util.Date;
import java.util.logging.*;
import java.io.*;
public class DaytimeUDPServer {
private final static int PORT = 13;
private final static Logger audit = Logger.getLogger("requests");
private final static Logger errors = Logger.getLogger("errors");
public static void main(String[] args) {
try (DatagramSocket socket = new DatagramSocket(PORT)) {
while (true) {
try {
DatagramPacket request = new DatagramPacket(new byte[1024], 1024);
socket.receive(request);
String daytime = new Date().toString();
byte[] data = daytime.getBytes("US-ASCII");
DatagramPacket response = new DatagramPacket(data, data.length,
request.getAddress(), request.getPort());
socket.send(response);
audit.info(daytime + " " + request.getAddress());
} catch (IOException | RuntimeException ex) {
errors.log(Level.SEVERE, ex.getMessage(), ex);
}
}
} catch (IOException ex) {
errors.log(Level.SEVERE, ex.getMessage(), ex);
}
}
}
4.DatagramPacket
1)接收数据包构造方法
public DatagramPacket(byte[] buffer, int length)
public DatagramPacket(byte[] buffer, int offset, int length)
2)大部分系统支持的数据包大小为8K,
3) 一般不要超过8K,虽然IPv4的理论值为65,507(IPv6
65,536)
2)发送数据包对象创建(des和port)
5.DatagramSocket
1)构造方法
public DatagramSocket() throws SocketException
public DatagramSocket(int port) throws SocketException
2)属性设置
SO_TIMEOUT:超时
SO_RCVBUF:接收缓存大小
SO_SNDBUF:发送缓存大小
SO_REUSEADDR:多个DatagramSocket绑定到同一个网络界面和端口
SO_BROADCAST:发送到广播地址和从广播地址接收
IP_TOS:数据包的优先级
6.DatagramChannel
功能与SocketChannel和ServerSocketChannel类似以非阻塞的方式处理网络通讯。
7.单播、广播、组播
1)单播(unicast):点对点通信
2)广播(Broadcast):一对所有通信
网络络对其中每一台主机发出的信号都进行无条件复制并转发。
3)组播/多播(multicast):一对多通信
4)组播 DNS (mDNS)工作在 IP 层面,使用5353端口。
它不请求 DNS 服务器,而是在局域网内广播,所有支持组播 DNS 的设备都会回复它自己的域名,如果没有指定自身机器名或者有冲突,那么新设备就会换个名字继续广播,直到名字可用,且被其他设备所接受。
5)组播作用
用法:将数据包发送给一组地址
一般不应用于Internet,而用于局域网
使用TTL(Time To Live)限制传输距离
6)组播地址和组
组播地址(multicast address)是称为组播组(multicast group)的一组主机的共享地址。
IPv4组播地址: 224.0.0.0 – 239.255.255.255; 所有地址以1110作为前4位。
2)组播组是一组共享一个组播地址的Internet主机
主播组地址可以是:225.0.0.0-238.255.255.255中的任一个。
任何发送给组播组地址的数据都会中继给组中的所有成员。
组是开放的,组中的成员主机可在任何时候加入和离开组。
组播组可以是临时的或永久的,永久的组播组分配的地址保持不变,不论组中是否有成员。临时组播组只在有成员时才存在。
7)组播编程例子
例程MulticastSniffer实现组播窃听器用于验证确实能够接收一个特定主机的组播数据:
从命令行中读取组播组名,根据这个主机名构造 一个InetAddress;创建一个MulticastSocket,尝试加入该主机名相应的组播组;如果尝试成功,MulticastSniffer从该Socket接收数据报,将内容显示在控制台。
大多数组播数据是二进制的,显示为文本时将无法理解。
运行:
java MulticastSniffer 239.255.255.250 1900
java MulticastSniffer 238.2.2.2 1900
import java.io.*;
import java.net.*;
public class MulticastSniffer {
public static void main(String[] args) {
InetAddress group = null;
int port = 0;
// read the address from the command line
try {
group = InetAddress.getByName(args[0]);
port = Integer.parseInt(args[1]);
} catch (ArrayIndexOutOfBoundsException | NumberFormatException
| UnknownHostException ex) {
System.err.println(
"Usage: java MulticastSniffer multicast_address port");
System.exit(1);
}
MulticastSocket ms = null;
try {
ms = new MulticastSocket(port);
ms.joinGroup(group);
byte[] buffer = new byte[8192];
while (true) {
DatagramPacket dp = new DatagramPacket(buffer, buffer.length);
ms.receive(dp);
String s = new String(dp.getData(), "8859_1");
System.out.println(s);
}
} catch (IOException ex) {
System.err.println(ex);
} finally {
if (ms != null) {
try {
ms.leaveGroup(group);
ms.close();
} catch (IOException ex) {}
}
}
}
}
MulticastSender实现发送组播数据:
从命令行读取输入(组播组的地址、端口号和一个可选的TTL);将一个字符串数据填充到字节数据data; 把这个数组放在DatagramPacket dp中;构造MulticastSocket ms,加入组ia;ms向组ia 发送10次数据报包dp;TTL设置为1,确保这个数据不会传输到本地子网之外;发送完数据后,ms离开组并自行关闭。
运行:
java MulticastSender 238.2.2.2 1900
import java.io.*;
import java.net.*;
public class MulticastSender {
public static void main(String[] args) {
InetAddress ia = null;
int port = 0;
byte ttl = (byte) 1;
// read the address from the command line
try {
ia = InetAddress.getByName(args[0]);
port = Integer.parseInt(args[1]);
if (args.length > 2) ttl = (byte) Integer.parseInt(args[2]);
} catch (NumberFormatException | IndexOutOfBoundsException
| UnknownHostException ex) {
System.err.println(ex);
System.err.println(
"Usage: java MulticastSender multicast_address port ttl");
System.exit(1);
}
byte[] data = "Here's some multicast data\r\n".getBytes();
DatagramPacket dp = new DatagramPacket(data, data.length, ia, port);
try (MulticastSocket ms = new MulticastSocket()) {
ms.setTimeToLive(ttl);
ms.joinGroup(ia);
for (int i = 1; i < 10; i++) {
ms.send(dp);
}
ms.leaveGroup(ia);
} catch (SocketException ex) {
System.err.println(ex);
} catch (IOException ex) {
System.err.println(ex);
}
}
}